package com.bestcem.xm.qdes.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bestcem.bp.xm.user.grpc.v1.services.User;
import com.bestcem.xm.bi.grpc.v1.enums.DataTypeEnum;
import com.bestcem.xm.bi.grpc.v1.enums.FuncEnum;
import com.bestcem.xm.bi.grpc.v1.enums.MultiFuncEnum;
import com.bestcem.xm.bi.grpc.v1.enums.OptionEnum;
import com.bestcem.xm.bi.grpc.v1.services.Filter;
import com.bestcem.xm.bi.grpc.v1.services.NewColumn;
import com.bestcem.xm.bi.grpc.v1.services.Pivot;
import com.bestcem.xm.common.core.domain.web.XmResultJson;
import com.bestcem.xm.common.core.enums.ReqErrorCodeEnum;
import com.bestcem.xm.common.core.exception.BusinessException;
import com.bestcem.xm.common.core.oss.enums.OssPathEnum;
import com.bestcem.xm.common.core.uitls.FileUtil;
import com.bestcem.xm.component.mybatis.base.BaseEntity;
import com.bestcem.xm.component.mybatis.service.impl.XmServiceImpl;
import com.bestcem.xm.component.security.context.SecurityContextHolder;
import com.bestcem.xm.componet.filestorage.service.StorageService;
import com.bestcem.xm.componet.redis.service.RedisService;
import com.bestcem.xm.deliver.grpc.v1.services.DeliverScenes;
import com.bestcem.xm.qdes.config.StringValue2FastJsonSerializer;
import com.bestcem.xm.qdes.controller.param.*;
import com.bestcem.xm.qdes.controller.vo.*;
import com.bestcem.xm.qdes.convert.ProjectEntryConvertMapper;
import com.bestcem.xm.qdes.convert.QdesProjectConvertMapper;
import com.bestcem.xm.qdes.convert.QstructConvertMapper;
import com.bestcem.xm.qdes.entity.*;
import com.bestcem.xm.qdes.enums.ProjectPermissionEnum;
import com.bestcem.xm.qdes.enums.QuestionDescTypeEnum;
import com.bestcem.xm.qdes.grpc.client.BiGrpcClient;
import com.bestcem.xm.qdes.grpc.client.DeliverGrpcClient;
import com.bestcem.xm.qdes.grpc.client.SurveyGrpcClient;
import com.bestcem.xm.qdes.grpc.client.UserGrpcClient;
import com.bestcem.xm.qdes.grpc.client.dto.OrgInfoDto;
import com.bestcem.xm.qdes.grpc.client.dto.SurveyRespondentDto;
import com.bestcem.xm.qdes.grpc.client.dto.UserDto;
import com.bestcem.xm.qdes.grpc.client.dto.WechatDeliverInfoDto;
import com.bestcem.xm.qdes.grpc.v1.services.ProjectEntryStatus;
import com.bestcem.xm.qdes.handler.impor.KeyWordHandler;
import com.bestcem.xm.qdes.mapper.*;
import com.bestcem.xm.qdes.mq.dto.CreateProjectDto;
import com.bestcem.xm.qdes.mq.dto.DeleteProjectDto;
import com.bestcem.xm.qdes.mq.dto.StatusDto;
import com.bestcem.xm.qdes.mq.send.biz.QdesMessageSendService;
import com.bestcem.xm.qdes.service.*;
import com.bestcem.xm.qdes.util.QdesConstant;
import com.bestcem.xm.survey.grpc.v1.services.RespondentSource;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.jetbrains.annotations.NotNull;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.bestcem.xm.qdes.enums.ProjectBadgeStatusEnum.PROJECTENTRY_BADGE_STATUS_READ;
import static com.bestcem.xm.qdes.enums.ProjectBadgeStatusEnum.PROJECTENTRY_BADGE_STATUS_UNREAD;
import static com.bestcem.xm.qdes.enums.ProjectPermissionEnum.DELIVER_TASK;
import static com.bestcem.xm.qdes.enums.QuestionDescTypeEnum.*;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_BLANK;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_CITY;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_GEO;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_MATRIX_BLANK;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_MATRIX_MULTIPLE;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_MATRIX_SCORE;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_MATRIX_SINGLE;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_MULTIPLE_BLANK;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_PROPORTION;
import static com.bestcem.xm.qdes.enums.QuestionTypeEnum.QUESTION_TYPE_SCORE;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectEntryCategory.PROJECTENTRY_CATEGORY_MANAGER;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectEntryPermType.PROJECTENTRY_PERM_TYPE_CUSTOM;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectPtype.PROJECT_PTYPE_SURVEY;
import static com.bestcem.xm.qdes.grpc.v1.services.ProjectStatus.*;
import static com.bestcem.xm.qdes.grpc.v1.services.QStructStatus.QSTRUCT_STATUS_DRAFT;
import static com.bestcem.xm.qdes.grpc.v1.services.QStructStatus.QSTRUCT_STATUS_RELEASE;
import static com.bestcem.xm.qdes.util.QdesConstant.QUESTION_TYPE_CASCADE;
import static com.bestcem.xm.qdes.util.QdesConstant.QUESTION_TYPE_EVALUATION;
import static com.bestcem.xm.qdes.util.QdesConstant.QUESTION_TYPE_MULTIPLE;
import static com.bestcem.xm.qdes.util.QdesConstant.QUESTION_TYPE_SINGLE;
import static com.bestcem.xm.qdes.util.QstructConstant.QS_KEYS;
import static com.bestcem.xm.qdes.util.QstructConstant.Q_KEYS;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentDataStatus.RESPONDENT_DATA_STATUS_FORMAL;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentStatus.RESPONDENT_STATUS_FINISH;

@Slf4j
@Service
@RequiredArgsConstructor
public class QdesProjectServiceImpl extends XmServiceImpl<QdesProjectMapper, QdesProject> implements QdesProjectService {
    private static final List<String> KEYWORDS = Arrays.asList("选项", "选项分组", "选项绑定", "矩阵行", "分类", "矩阵行分组", "矩阵行绑定", "排他设置");
    private static final String OPEN_GROUPING = "openGrouping";
    private static final String OPEN_CUSTOM_CID = "openCustomCid";
    private static final String CUSTOM_ATTR_FILE_PATH = "file/custom_attr.json";
    private static final String STRUCT_DEFAULT_DATA_FILE_PATH = "file/struct_default_data.json";
    private static final String PAGE_FILE_PATH = "file/struct_default_data_p_items.json";
    public static final String SURVEY_IP_CNT_FORMAT_KEY = "Survey:%s:%s:ip-cnt";
    private static final String ROOT_FILE_PATH = System.getProperty("user.dir");

    //手动事务
    //private @NonNull DataSourceTransactionManager dataSourceTransactionManager;
    //private @NonNull TransactionDefinition transactionDefinition;

    private @NonNull BiGrpcClient biGrpcClient;
    private @NonNull UserGrpcClient userGrpcClient;
    private @NonNull DeliverGrpcClient deliverGrpcClient;
    private @NonNull SurveyGrpcClient surveyGrpcClient;

    private @NonNull QdesProjectEntryMapper qdesProjectentryMapper;
    private @NonNull QdesProjectMapper qdesProjectMapper;
    private @NonNull QdesProjectSettingMapper qdesProjectsettingMapper;
    private @NonNull QdesQstructMapper qdesQstructMapper;
    private @NonNull QdesProjectConvertMapper qdesProjectConvertMapper;
    private @NonNull ProjectEntryConvertMapper projectEntryConvertMapper;
    private @NonNull QstructConvertMapper qstructConvertMapper;
    private @NonNull QdesQuestionMapper qdesQuestionMapper;
    private @NonNull QdesProjectEntryMapper projectentryMapper;
    private @NonNull QdesProjectExtraAttrMapper projectextraattrMapper;
    private @NonNull ObjectMapper objectMapper;

    private @NonNull RedisService redisService;
    private @NonNull ServiceSequenceService serviceSequenceService;
    private @NonNull QdesQuestionService questionService;
    private @NonNull QdesQstructService qdesQstructService;
    private @NonNull QdesLibProjectService qdesLibprojectService;
    private @NonNull QdesMessageSendService qdesMessageSendService;

    //private @NonNull OssService ossService;
    private @NonNull StorageService storageService;
    private @NonNull List<KeyWordHandler> keyWordHandlers;

    @Resource(name = "releaseProjectTaskExecutor")
    private ThreadPoolTaskExecutor releaseProjectTaskExecutor;

    @Override
    public QdesProjectRespNumVo getProjectRespNum(String id) {
        QdesProject project = baseMapper.selectById(id);
        if (project == null) {
            log.error("The project is null, pid = {}", id);
            throw new BusinessException("项目为空，请检查。");
        }
        // 通过查询BI获取答卷统计数据
//        String worktableInfo = project.getWorktableInfo();
//        List<Integer> surveyFinishedList = ListUtil.of(0, 0, 0);
//        if (StringUtils.isEmpty(worktableInfo)) {
//            return new QdesProjectRespNumVo(surveyFinishedList);
//        }
//        JSONObject worktableIdJson = JSONObject.parseObject(worktableInfo);
//        if (worktableIdJson == null || worktableIdJson.isEmpty()) {
//            return new QdesProjectRespNumVo(surveyFinishedList);
//        }
//        String detailRspdCountKey = "QDES:detail_rspd_count_" + id;
//        QdesProjectRespNumVo qdesProjectRespNumVo = (QdesProjectRespNumVo) redisService.get(detailRspdCountKey);
//        if (qdesProjectRespNumVo != null) {
//            return qdesProjectRespNumVo;
//        }
//        // 获取答题详细计数，今天/本周/本月
//        String worktableId = worktableIdJson.getString("worktable_id");
//        String seqColId = worktableIdJson.getString("seq_col_id");
//        String dayStart = QdesDateUtil.getBeginOfDay();
//        String weekStart = QdesDateUtil.getBeginOfWeek();
//        String monthStart = QdesDateUtil.getBeginOfMonth();
//        qdesProjectRespNumVo = new QdesProjectRespNumVo();
//        List<Integer> nums = new ArrayList<>();
//        nums.add(getPivotData(dayStart, worktableId, seqColId));
//        nums.add(getPivotData(weekStart, worktableId, seqColId));
//        nums.add(getPivotData(monthStart, worktableId, seqColId));
        //List<Integer> nums = (List<Integer>) getFinishedRspdCount(id, RESPONDENT_DATA_STATUS_FORMAL.getNumber(), RESPONDENT_STATUS_FINISH.getNumber()).getOrDefault("survey_finished_list", ListUtil.of(0, 0, 0));
        Collection<Integer> surveyFinishedList = getFinishedRspdCountV2(id, RESPONDENT_DATA_STATUS_FORMAL.getNumber(), RESPONDENT_STATUS_FINISH.getNumber()).get("survey_finished_list");
        List<Integer> nums = new ArrayList<>(surveyFinishedList);
        QdesProjectRespNumVo qdesProjectRespNumVo = new QdesProjectRespNumVo();
        qdesProjectRespNumVo.setSurveyFinishedList(nums);
        return qdesProjectRespNumVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProjectTitle(String id, String title) {
        QdesProject project = baseMapper.selectById(id);
        if (project == null) {
            log.error("The project is null, pid = {}", id);
            throw new BusinessException("项目为空，请检查。");
        }
        project.setTitle(title);
        baseMapper.updateById(project);
        // 修改entery中的title
        LambdaUpdateWrapper<QdesProjectEntry> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件-更新
        updateWrapper
                .set(QdesProjectEntry::getTitle, title)
                .set(QdesProjectEntry::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProjectEntry::getProjectId, id);
        qdesProjectentryMapper.update(null, updateWrapper);
        // 删除缓存中的项目
        //redisService.del("survey_get_project#" + id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProjectRead(String id, ProjectReadParam param) {
        QdesProject project = baseMapper.selectById(id);
        if (project == null) {
            log.error("The project is null, pid = {}", id);
            throw new BusinessException("项目为空，请检查。");
        }
        int oldStatus = param.getBadgeStatus() == PROJECTENTRY_BADGE_STATUS_UNREAD.getValue()
                ? PROJECTENTRY_BADGE_STATUS_READ.getValue() : PROJECTENTRY_BADGE_STATUS_UNREAD.getValue();
        LambdaUpdateWrapper<QdesProjectEntry> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件-更新
        updateWrapper
                .set(QdesProjectEntry::getBadgeStatus, param.getBadgeStatus())
                .set(QdesProjectEntry::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProjectEntry::getUserId, param.getUserId())
                .eq(QdesProjectEntry::getBadgeStatus, oldStatus)
                .eq(QdesProjectEntry::getProjectId, id);
        qdesProjectentryMapper.update(null, updateWrapper);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProjectFolder(String id, ProjectFolderIdParam param) {
        QdesProject project = baseMapper.selectById(id);
        if (project == null) {
            log.error("The project is null, pid = {}", id);
            throw new BusinessException("项目为空，请检查。");
        }
        // 修改entery中的属组
        LambdaUpdateWrapper<QdesProjectEntry> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件-更新
        updateWrapper
                .set(QdesProjectEntry::getFolderId, param.getFolderId())
                .set(QdesProjectEntry::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProjectEntry::getUserId, param.getUserId())
                .eq(QdesProjectEntry::getProjectId, id);
        qdesProjectentryMapper.update(null, updateWrapper);
    }

    @Override
    //@Transactional(rollbackFor = Exception.class)
    public QdesProjectVo createProject(ProjectCreateParam param) {
        log.info("Now to createProject, param = {}", param);
        // 创建项目
        QdesProject newProject = new QdesProject();
        newProject.setTitle(param.getTitle());
        newProject.setCreator(param.getUserId());
        newProject.setCompanyId(param.getOrgId());
        newProject.setStatus(PROJECT_STATUS_CREATED.getNumber());
        newProject.setIsCopyDemo(false);
        newProject.setPtype(PROJECT_PTYPE_SURVEY.getNumber());
        newProject.setReleaseVer(0);
        QdesProject project = createOneProject(newProject, false);
        String adminRoleId = getAdminRoleId(param.getOrgId());
        List<String> seniorIds = userGrpcClient.getSeniorAdminIdByOrgId(param.getOrgId());
        List<String> allUsers = new ArrayList<>();
        allUsers.add(param.getUserId());
        if (StringUtils.isNotEmpty(adminRoleId)) {
            allUsers.add(adminRoleId);
        }
        if (CollectionUtils.isNotEmpty(seniorIds)) {
            allUsers.addAll(seniorIds);
        }
        allUsers = allUsers.stream().distinct().collect(Collectors.toList());
        for (String userId : allUsers) {
            String permissions = JSONObject.toJSONString(Arrays.stream(ProjectPermissionEnum.values())
                    .map(ProjectPermissionEnum::getValue).collect(Collectors.toList()));
            QdesProjectEntry projectentry = new QdesProjectEntry();
            projectentry.setOrgId(project.getCompanyId());
            projectentry.setProjectId(project.getId());
            projectentry.setCategory(PROJECTENTRY_CATEGORY_MANAGER.getNumber());
            projectentry.setPermissionsArry(permissions);
            projectentry.setUserId(userId);
            projectentry.setTitle(project.getTitle());
            projectentry.setStatus(ProjectEntryStatus.PROJECTENTRY_STATUS_CREATED.getNumber());
            projectentry.setCreatorId(param.getUserId());
            projectentry.setPermType(PROJECTENTRY_PERM_TYPE_CUSTOM.getNumber());
            projectentry.setFolderId(Optional.ofNullable(param.getFolderId()).orElse(""));
            projectentry.setBadgeStatus(PROJECTENTRY_BADGE_STATUS_READ.getValue());
            qdesProjectentryMapper.insert(projectentry);
        }
        log.info("Now to create wechat_deliver");
        deliverGrpcClient.createWechatdeliverWithName(SecurityContextHolder.getOrgId(), SecurityContextHolder.getUserId(), project.getId(), "微信投放");
        QdesProjectVo projectVo = qdesProjectConvertMapper.project2Vo(project);
        projectVo.setFolderId(param.getFolderId());
        projectVo.setOrgId(param.getOrgId());
        return projectVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public QdesProjectDeleteVo deleteProject(ProjectDeleteParam param) {
        log.info("Now to deleteProject, param = {}", param);
        QdesProject project = baseMapper.selectById(param.getPid());
        if (project == null) {
            log.error("The project is null, pid = {}", param.getPid());
            throw new BusinessException("项目为空，请检查。");
        }
        int status = PROJECT_STATUS_DELETED.getNumber();
        project.setStatus(status);
        project.setUpdatedDt(LocalDateTime.now());
        baseMapper.updateById(project);
        // 删除projectentry
        LambdaQueryWrapper<QdesProjectEntry> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesProjectEntry::getProjectId, param.getPid());
        qdesProjectentryMapper.delete(queryWrapper);

        // 发送mq
        StatusDto statusDto = new StatusDto().setCurrent(status).setPrev(project.getStatus()).setProjectId(param.getPid());
        qdesMessageSendService.projectStatusChangedSend(statusDto);
        qdesMessageSendService.projectUpdatedSend(statusDto);
        DeleteProjectDto delecteProjectDto = new DeleteProjectDto().setId(param.getPid()).setOrgId(project.getCompanyId());
        qdesMessageSendService.projectDeletedSend(delecteProjectDto);
        QdesProjectDeleteVo projectDeleteVo = new QdesProjectDeleteVo();
        projectDeleteVo.setDeletedCount(1);
        return projectDeleteVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishProject(ProjectFinishParam param) {
        log.info("Now to finishProject, param = {}", param);
        QdesProject project = baseMapper.selectById(param.getProjectId());
        if (project == null) {
            log.error("The project is null, pid = {}", param.getProjectId());
            throw new BusinessException("项目为空，请检查。");
        }
        // 更新项目
        LambdaUpdateWrapper<QdesProject> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper
                .set(QdesProject::getStatus, PROJECT_STATUS_FINISHED.getNumber())
                .set(QdesProject::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProject::getId, param.getProjectId());
        baseMapper.update(null, updateWrapper);
        // 更新entry
        LambdaUpdateWrapper<QdesProjectEntry> entryWrapper = new LambdaUpdateWrapper<>();
        entryWrapper
                .set(QdesProjectEntry::getStatus, PROJECT_STATUS_FINISHED.getNumber())
                .set(QdesProjectEntry::getUpdatedDt, LocalDateTime.now())
                .eq(QdesProjectEntry::getProjectId, param.getProjectId());
        qdesProjectentryMapper.update(null, entryWrapper);
        // 发送mq
        int status = PROJECT_STATUS_FINISHED.getNumber();
        StatusDto statusDto = new StatusDto().setCurrent(status).setPrev(project.getStatus()).setProjectId(param.getProjectId());
        qdesMessageSendService.projectStatusChangedSend(statusDto);
        qdesMessageSendService.projectUpdatedSend(statusDto);
        // 更新struct
        LambdaUpdateWrapper<QdesQstruct> structWrapper = new LambdaUpdateWrapper<>();
        structWrapper
                .set(QdesQstruct::getWithdrawDt, LocalDateTime.now())
                .set(QdesQstruct::getUpdatedDt, LocalDateTime.now())
                .eq(QdesQstruct::getProjectId, param.getProjectId())
                .eq(QdesQstruct::getVersion, project.getReleaseVer());
        qdesQstructMapper.update(null, structWrapper);
    }

    @Override
    public QdesProjectDetailVo getProjectDetail(String id, String userId, String orgId) {
        log.info("Now to getProjectDetail, id = {}", id);
        QdesProject project = baseMapper.selectById(id);
        if (project == null) {
            log.error("The project is null, id = {}", id);
            throw new BusinessException("项目为空，请检查。");
        }
        LambdaQueryWrapper<QdesProjectEntry> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesProjectEntry::getProjectId, id).eq(QdesProjectEntry::getUserId, userId)
                .last("limit 1");
        QdesProjectEntry qdesProjectentry = qdesProjectentryMapper.selectOne(queryWrapper);
        QdesProjectDetailVo detailVo = new QdesProjectDetailVo();
        Double nps = null;
        List<Double> npsDetailList = new ArrayList<>();
        Double sat = null;
        if (qdesProjectentry != null) {
            if (PROJECT_STATUS_CREATED.getNumber() == project.getStatus() || project.getWorktableInfo() == null) {
                log.info("No nedd to get nps");
            } else {
                //{"cal_score": [4, 5], "seq_col_id": "630f042e5283240018c7b8ea", "worktable_id": "630f042e5283240018c7b8e8", "first_nps_col": "", "first_sat_col": "12_13", "first_nps_col_id": "", "first_sat_col_id": "630f042e5283240018c7b914"}
                String worktableInfo = project.getWorktableInfo();
                JSONObject worktableIdJson = JSONObject.parseObject(worktableInfo);
                String worktableId = worktableIdJson == null ? "" : worktableIdJson.getString("worktable_id");
                String seqColId = worktableIdJson == null ? "" : worktableIdJson.getString("seq_col_id");
                String npsCol = worktableIdJson == null ? "" : worktableIdJson.getString("first_nps_col");
                String satCol = worktableIdJson == null ? "" : worktableIdJson.getString("first_sat_col");
                Object calScore = worktableIdJson == null ? null : worktableIdJson.get("cal_score");

                Map<String, List<Object>> npsRuleDict = new HashMap<>();
                npsRuleDict.put("nps_Promoter", Arrays.asList("gte", 9));
                npsRuleDict.put("nps_Detractors", Arrays.asList("lte", 6));
                npsRuleDict.put("nps_Passives", Arrays.asList("in", Arrays.asList(7, 8)));
                npsRuleDict.put("nps_Total", new ArrayList<>());

                Map<String, List<Object>> satRuleDict = new HashMap<>();
                satRuleDict.put("Sat", Arrays.asList("in", calScore));
                satRuleDict.put("Total", new ArrayList<>());
                if (StringUtils.isEmpty(satCol) || ObjectUtil.isEmpty(calScore)) {
                    sat = null;
                } else {
                    Map<String, Integer> satDict = getSatisfactionNpsByPid(worktableId, satCol, satRuleDict, seqColId);
                    if (satDict.get("Total") == null || satDict.get("Total") == 0) {
                        sat = null;
                    } else {
                        BigDecimal satDecimal = new BigDecimal(satDict.get("Sat") * 100 / satDict.get("Total"));
                        sat = satDecimal.setScale(1, RoundingMode.HALF_UP).doubleValue();
                    }
                }
                if (StringUtils.isEmpty(npsCol)) {
                    nps = null;
                } else {
                    Map<String, Integer> npsDict = getSatisfactionNpsByPid(worktableId, npsCol, npsRuleDict, seqColId);
                    if (npsDict.get("Total") == null || npsDict.get("Total") == 0) {
                        nps = null;
                    } else {
                        BigDecimal npsDecimal = new BigDecimal(npsDict.get("Promoter") / npsDict.get("Total") * 100
                                - npsDict.get("Detractors") / npsDict.get("Total") * 100);
                        nps = npsDecimal.setScale(1, RoundingMode.HALF_UP).doubleValue();
                        npsDetailList.add(new BigDecimal(npsDict.get("Detractors") / npsDict.get("Total") * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                        npsDetailList.add(new BigDecimal(npsDict.get("Passives") / npsDict.get("Total") * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                        npsDetailList.add(new BigDecimal(npsDict.get("Promoter") / npsDict.get("Total") * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                    }
                }
            }
            //boolean checkPermission = checkCurrentChecker(userId, orgId);
            boolean checkPermission = userGrpcClient.checkCurrentChecker(userId, orgId);
            if (checkPermission) {
                setPermission(qdesProjectentry, userId);
            }
            detailVo = projectEntryConvertMapper.project2Vo(qdesProjectentry);
            detailVo.setIsCopyDemo(project.getIsCopyDemo());
            detailVo.setSatisfaction(sat);
            detailVo.setNps(nps);
            detailVo.setNpsDetailList(CollectionUtils.isEmpty(npsDetailList) ? Arrays.asList(null, null, null) : npsDetailList);
        }
        // 如果当前用户没有项目权限 并且 是项目审核者
        if (qdesProjectentry == null) {
            //boolean checkPermission = checkCurrentChecker(userId, orgId);
            boolean checkPermission = userGrpcClient.checkCurrentChecker(userId, orgId);
            if (checkPermission) {
                // 如果是审核者，但是没有被分享，则通过公司管理员查询出所有项目
                String adminRoleId = getAdminRoleId(orgId);
                LambdaQueryWrapper<QdesProjectEntry> entryWrapper = Wrappers.lambdaQuery();
                entryWrapper.eq(QdesProjectEntry::getProjectId, id).eq(StrUtil.isNotEmpty(adminRoleId), QdesProjectEntry::getUserId, adminRoleId)
                        .last("limit 1");
                qdesProjectentry = qdesProjectentryMapper.selectOne(entryWrapper);
                setPermission(qdesProjectentry, userId);
                detailVo = projectEntryConvertMapper.project2Vo(qdesProjectentry);
            }
        }

        detailVo.setIsCopyDemo(project.getIsCopyDemo());
        // 每次发布后，获取最新的问卷version，黑白名单保持一致
        detailVo.setVersion(project.getVersion());
        detailVo.setReleaseVer(project.getReleaseVer());
        return detailVo;
    }

    @Override
    public List<JSONObject> getProjectQuestions(String id, ProjectQuestionParam param) {
        List<JSONObject> questionList = new ArrayList<>();
        QdesProject project = baseMapper.selectById(id);
        if (project == null) {
            log.error("The project is null, pid = {}", id);
            throw new BusinessException("项目为空，请检查。");
        }
        int version = project.getVersion();
        // 获取struct
        LambdaQueryWrapper<QdesQstruct> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesQstruct::getProjectId, id).eq(QdesQstruct::getVersion, version).last("limit 1");
        QdesQstruct qdesQstruct = qdesQstructMapper.selectOne(queryWrapper);
        // 获取questions
        LambdaQueryWrapper<QdesQuestion> questionWrapper = Wrappers.lambdaQuery();
        questionWrapper.eq(QdesQuestion::getStructId, qdesQstruct.getId());
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        fillQstructData(qdesQstruct, qdesQuestions);
        /*String sData = qdesQstruct.getData();
        if (StringUtils.isNotEmpty(sData)) {
            Map<Integer, String> questions = new HashMap<>();
            qdesQuestions.forEach(qdes -> {
                JSONObject dataJson = new JSONObject();
                String data = qdes.getData();
                if (StringUtils.isEmpty(data)) {
                    dataJson.put("id", qdes.getId());
                } else {
                    dataJson = JSONObject.parseObject(data);
                    dataJson.put("id", qdes.getId());
                }
                questions.put(qdes.getGid(), JSONObject.toJSONString(dataJson));
            });
            qdesQstruct.setData(parseItem(questions, sData));
        }*/
        // 重置顺序
        if (StringUtils.isNotEmpty(qdesQstruct.getData())) {
            JSONObject dataJson = JSONObject.parseObject(qdesQstruct.getData());
            resetSeq(dataJson);
            List<String> attrs = Arrays.asList("showQuestionCid", "openCustomCid");
            String customAttr = project.getCustomAttr();
            if (StringUtils.isNotEmpty(customAttr)) {
                JSONObject jsonAttr = JSONObject.parseObject(customAttr);
                dataJson.put("limit_device", jsonAttr.getOrDefault("devOnce", false));
                attrs.forEach(attr -> dataJson.put(attr, jsonAttr.getOrDefault(attr, false)));
            } else {
                attrs.forEach(attr -> dataJson.put(attr, false));
            }
            return walkQstructQuestions(dataJson);
        }
        return questionList;
    }

    private String parseItem(Map<Integer, String> questions, String itemStr) {
        if (NumberUtils.isDigits(itemStr)) {
            return questions.get(Integer.valueOf(itemStr));
        }
        JSONObject item = JSONObject.parseObject(itemStr);
        String type = item.getString("type");
        if (StringUtils.isEmpty(type)) {
            Integer gid = item.getInteger("gid");
            if (gid != null) {
                JSONObject item2 = JSONObject.parseObject(questions.get(gid));
                if (item2 != null) {
                    //item.keySet().forEach(key -> {
                    //    item2.put(key, item.get(key));
                    //});
                    item2.putAll(item);
                } else {
                    item2 = new JSONObject();
                }
                return JSONObject.toJSONString(item2);
            }
        } else if ("G".equals(type)) {
            if (item.get("items") == null) {
                throw new BusinessException("group[gid=" + item.get("gid") + "] must have :items field");
            }
            List<String> array = JSONObject.parseArray(item.getString("items"), String.class);
            List<JSONObject> collect = array.stream().map(oneItem -> JSONObject.parseObject(parseItem(questions,
                    oneItem))).filter(oneItem -> !oneItem.isEmpty()).collect(Collectors.toList());
            item.put("items", collect);
            return JSONObject.toJSONString(item);
        } else if ("Q".equals(type)) {
            JSONObject valueJo = JSONObject.parseObject(questions.get(item.getInteger("gid")));
            item.put("id", valueJo == null ? null : valueJo.get("id"));
            return JSONObject.toJSONString(item);
        }
        return itemStr;
    }

    @Override
    public void resetSeq(JSONObject data) {
        if (data == null) {
            return;
        }
        for (JSONObject q : walkQstructQuestions(data)) {
            JSONArray items = data.getJSONArray("items");
            if (items != null) {
                for (int ind = 0; ind < items.size(); ind++) {
                    JSONObject option = items.getJSONObject(ind);
                    if (option.get("seq") != null) {
                        option.put("seq", option.get("gid"));
                    }
                }
            }

            JSONArray rowsItems = q.getJSONArray("rows_items");
            if (rowsItems != null) {
                for (int ind = 0; ind < rowsItems.size(); ind++) {
                    JSONObject row = rowsItems.getJSONObject(ind);
                    if (row.get("seq") != null) {
                        row.put("seq", row.get("gid"));
                    }
                }
            }
        }
    }

    @Override
    public JSONObject initWechatQrcode(String id) {
        WechatDeliverInfoDto wechat_deliver = deliverGrpcClient.queryWechatDeliverByParam(SecurityContextHolder.getOrgId(), id);
        String wechat_deliver_id;
        if (Objects.isNull(wechat_deliver)) {
            wechat_deliver_id = deliverGrpcClient.createWechatdeliverWithName(SecurityContextHolder.getOrgId(), SecurityContextHolder.getUserId(), id, "微信投放");
        } else {
            wechat_deliver_id = wechat_deliver.getDeliverId();
        }

        OrgInfoDto org = userGrpcClient.getOrganization(SecurityContextHolder.getOrgId());
        log.info("initWechatQrcode orgCode=" + org.getCode());
        Map<String, String> kwargs = new HashMap<>();
        kwargs.put("source", String.valueOf(RespondentSource.RESPONDENT_SOURCE_WECHAT.getNumber()));
        kwargs.put("code", "");
        kwargs.put("scenes", String.valueOf(DeliverScenes.DELIVER_SCENES_QRCODE.getNumber()));

        String wxshare_qrcode_img = deliverGrpcClient.getWxShareQrcodeImgurl(SecurityContextHolder.getOrgId(), id, wechat_deliver_id, org.getCode(), kwargs);

        QdesProject qdesProject = baseMapper.selectById(id);
        JSONObject customAttr = JSONObject.parseObject(qdesProject.getCustomAttr());
        customAttr.put("wxShare_qrcode_img", wxshare_qrcode_img);
        customAttr.put("wxShare_qrcode_img_id", "");

        QdesProject update = new QdesProject();
        update.setId(id);
        update.setCustomAttr(JSON.toJSONString(customAttr));
        baseMapper.updateById(update);

        //redisService.del("survey_get_project#" + id);
        JSONObject ret = new JSONObject();
        ret.put("share_url", wxshare_qrcode_img);
        return ret;
    }

    @Override
    public QdesProject getReleasedProject(String projectId, Integer version) throws JsonProcessingException {
        QdesProject project = baseMapper.selectById(projectId);
        if (Objects.isNull(version) || version.equals(0)) {
            if (Objects.isNull(project.getReleaseVer()) || project.getReleaseVer().equals(0)) {
                log.error("getReleasedProject no released project[id={}] found", projectId);
                return null;
            }
            version = project.getReleaseVer();
        }

        setQstructJson2Project(projectId, project, version);
        project.setVersion(version);
        return project;
    }

    @Override
    public QdesQstruct getQstructByVersion(String projectId, Integer version) {
        // 获取struct
        LambdaQueryWrapper<QdesQstruct> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesQstruct::getProjectId, projectId)
                .eq(QdesQstruct::getVersion, version)
                .last("limit 1");
        QdesQstruct struct = qdesQstructMapper.selectOne(queryWrapper);
        if (Objects.isNull(struct)) {
            log.error("getQstructByVersion qstruct[projectID={},version={}] not found", projectId, version);
            return null;
        }
        // 获取questions
        LambdaQueryWrapper<QdesQuestion> questionWrapper = Wrappers.lambdaQuery();
        questionWrapper.eq(QdesQuestion::getStructId, struct.getId());
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        fillQstructData(struct, qdesQuestions);
        return struct;
    }

    @Override
    public QdesProject getCurrentProject(String projectId) throws JsonProcessingException {
        QdesProject project = baseMapper.selectById(projectId);
        Integer version = project.getVersion();

        setQstructJson2Project(projectId, project, version);
        return project;
    }

    @Override
    public JSONObject getProjectOwner(String pid, String uid, String orgId) {
        log.info("now to getProjectOwner, pid = {}, uid = {}, orgId = {}", pid, uid, orgId);
        QdesProject project = baseMapper.selectById(pid);
        if (project == null) {
            throw new BusinessException("The project not found");
        }
        String creator = project.getCreator();
        JSONObject re = new JSONObject();
        if (StringUtils.isNotEmpty(creator)) {
            UserDto user = userGrpcClient.getUser(creator);
            if (user != null) {
                re.put("id", user.getId());
                re.put("email", user.getEmail());
                re.put("mobile", user.getMobile());
                re.put("userName", user.getUsername());
                return re;
            }
        }
        re.put("id", "");
        re.put("email", "");
        re.put("mobile", "");
        re.put("userName", "");
        return re;
    }

    @Override
    public List<JSONObject> getTimeCost(String pid, String gid) {
        List<JSONObject> rets = new ArrayList<>();
        try {
            QdesProject proj = this.getCurrentProject(pid);
            List<QdesQuestion> questionList = questionService.getQuestionList(JSONObject.parseObject(proj.getQstruct()).getString("id"), null);
            Map<String, QdesQuestion> question_dict = new HashMap<>();
            questionList.forEach(q -> question_dict.put(String.valueOf(q.getGid()), q));
            List<QdesQuestion> question_list = new ArrayList<>();
            for (JSONObject q : walkQstructQuestions(JSONObject.parseObject(proj.getQstruct()).getJSONObject("data"))) {
                if (ObjectUtil.isNotEmpty(question_dict.get(q.getString("gid")))) {
                    question_list.add(question_dict.get(q.getString("gid")));
                }
            }
            for (QdesQuestion q : question_list) {
                JSONObject question_data = JSONObject.parseObject(q.getData());
                Object default_q_timecost = question_data.get("default_q_timecost");
                Object q_timecost = question_data.get("q_timecost");
                String qtitle = HtmlUtil.cleanHtmlTag(question_data.getString("title"));
                if (ObjectUtil.isEmpty(default_q_timecost)) {
                    default_q_timecost = updateQuestionDefaultTimecost(q);
                }
                JSONObject tmp = new JSONObject();
                tmp.put("gid", q.getGid());
                tmp.put("idx", question_data.get("_index"));
                tmp.put("cid", question_data.get("cid"));
                tmp.put("title", qtitle);
                tmp.put("dqt", default_q_timecost);
                //注意：题目自定义时长，为None时不需要返回给前端（前端根据该字段是否存在，没有会取默认时长）
                if (ObjectUtil.isNotEmpty(q_timecost)) {
                    if (Integer.parseInt(String.valueOf(q_timecost)) == 0) {
                        q_timecost = "";
                    }
                    tmp.put("qt", q_timecost);
                }
                rets.add(tmp);
            }
        } catch (JsonProcessingException e) {
            log.error("JsonProcessingException", e);
        }
        return rets;
    }

    @Override
    public void postTimeCost(String pid, JSONObject params) {
        //问卷题目自定义时长
        try {
            QdesProject proj = this.getCurrentProject(pid);
            //更新草稿
            JSONObject qstruct = JSONObject.parseObject(proj.getQstruct());
            updateProjectQuestionTimecost0(qstruct, params);

            if (proj.getStatus() == PROJECT_STATUS_ACTIVATED.getNumber()) {
                //# 问卷发布中
                //# 更新 发布中问卷的题目自定义题目时长
                QdesProject project = getReleasedProject(pid, proj.getReleaseVer());
                updateProjectQuestionTimecost0(JSONObject.parseObject(project.getQstruct()), params);
                genReleaseProject(project.getId(), project.getReleaseVer());
            }
        } catch (Exception e) {
            throw new BusinessException("update attribute into project.js failed");
        }
    }

    private void updateProjectQuestionTimecost0(JSONObject qstruct, JSONObject params) {
        String struct_oid = qstruct.getString("id");
        List<QdesQuestion> question_list = questionService.getQuestionList(struct_oid, params.keySet());
        for (QdesQuestion q : question_list) {
            JSONObject question_data = JSONObject.parseObject(q.getData());
            Object q_timecost = params.get(String.valueOf(q.getGid()));
            if (ObjectUtil.isNotEmpty(q_timecost)) {
                if (q_timecost.toString().trim().isEmpty()) {
                    q_timecost = 0;
                } else {
                    try {
                        q_timecost = Integer.parseInt(q_timecost.toString().trim());
                    } catch (Exception E) {
                        q_timecost = questionService.calculateTimecost(question_data);
                    }
                }
                question_data.put("q_timecost", q_timecost);
                q.setData(String.valueOf(question_data));
                qdesQuestionMapper.update(q, new LambdaUpdateWrapper<QdesQuestion>().eq(QdesQuestion::getId, q.getId()));
            } else {
                question_data.remove("q_timecost");
                q.setData(String.valueOf(question_data));
                qdesQuestionMapper.update(q, new LambdaUpdateWrapper<QdesQuestion>().eq(QdesQuestion::getId, q.getId()));
            }
        }
    }

    private int updateQuestionDefaultTimecost(QdesQuestion question) {
        JSONObject question_data = JSONObject.parseObject(question.getData());
        int default_question_timecost = questionService.calculateTimecost(question_data);
        question_data.put("default_q_timecost", default_question_timecost);
        question.setData(String.valueOf(question_data));
        qdesQuestionMapper.update(question, new LambdaUpdateWrapper<QdesQuestion>().eq(QdesQuestion::getId, question.getId()));
        return default_question_timecost;
    }


    private void setQstructJson2Project(String projectId, QdesProject project, Integer version) throws JsonProcessingException {
        QdesQstruct qstructdb = getQstructByVersion(projectId, version);
        QstructsDetailVo qstruct = qstructConvertMapper.qstruct2Vo(qstructdb);
        JSONObject dataJson = JSONObject.parseObject(qstruct.getData());
        resetSeq(dataJson);

        Object devOnce = JSONObject.parseObject(project.getCustomAttr()).getOrDefault("devOnce", false);
        dataJson.put("limit_device", devOnce);

        Object showQuestionCid = JSONObject.parseObject(project.getCustomAttr()).getOrDefault("showQuestionCid", false);
        dataJson.put("showQuestionCid", showQuestionCid);

        Object openCustomCid = JSONObject.parseObject(project.getCustomAttr()).getOrDefault("openCustomCid", false);
        dataJson.put("openCustomCid", openCustomCid);

        qstruct.setData(dataJson.toJSONString());
        //ObjectMapper objectMapper = new ObjectMapper();
        //objectMapper.registerModule(new JavaTimeModule());

        String qstructjson = objectMapper.writeValueAsString(qstruct);
        project.setQstruct(qstructjson);
    }

    @Override
    public List<JSONObject> walkQstructQuestions(JSONObject data) {
        List<JSONObject> questionList = new ArrayList<>();
        if (data == null) {
            return questionList;
        }
        String itemsStr = data.getString("items");
        if (StringUtils.isEmpty(itemsStr)) {
            return questionList;
        }
        List<String> items = JSONArray.parseArray(itemsStr, String.class);
        for (String item : items) {
            if (NumberUtils.isDigits(item)) {
                continue;
            }
            JSONObject itemJs = JSONObject.parseObject(item);
            String type = itemJs.getString("type");
            if ("Q".equals(type)) {
                if (itemJs.getString("items") != null) {
                    itemJs.put("items", JSONArray.parseArray(itemJs.getString("items")));
                }
                questionList.add(itemJs);
            } else if ("G".equals(type)) {
                questionList.addAll(walkQstructQuestions(itemJs));
            }
        }
        return questionList;
    }

    private Map<String, Integer> getSatisfactionNpsByPid(String worktableId, String satCol, Map<String, List<Object>> satRuleDict, String seqColId) {
        Map<String, Integer> resultDict = new HashMap<>();
        satRuleDict.keySet().forEach(key -> {
            // 组装filter
            List<Object> rule = satRuleDict.get(key);
            List<Filter> filters = new ArrayList<>();
            String name = key;
            if (key.contains("nps")) {
                // 计算卡片nps时，计算所有数据，不筛选成功完成的数据
                name = key.split("_")[0];
            } else {
                filters.add(Filter.newBuilder().setCol("status").setOption(OptionEnum.Option.EQ)
                        .addValues("成功完成")
                        .setDataType(DataTypeEnum.DataType.STRING).build());
            }
            if (!CollUtil.isEmpty(rule)) {
                String option = rule.get(0).toString();
                OptionEnum.Option op = null;
                for (OptionEnum.Option option1 : OptionEnum.Option.values()) {
                    if (option1.name().equalsIgnoreCase(option)) {
                        op = option1;
                    }
                }
                filters.add(Filter.newBuilder().setCol(satCol).setOption(op)
                        .addAllValues(Convert.toList(String.class, rule.get(1)))
                        .setDataType(DataTypeEnum.DataType.NUMBER).build());
            } else {
                filters.add(Filter.newBuilder().setCol(satCol).setOption(OptionEnum.Option.NEP)
                        .addValues("")
                        .setDataType(DataTypeEnum.DataType.NUMBER).build());
            }
            // 组装NewColumn
            NewColumn countColumn = NewColumn.newBuilder().setCol("total_count").setName("total_count")
                    .setColumnId(seqColId).setDataType(DataTypeEnum.DataType.NUMBER).build();
            // python中Func是0对应的count， java中是1对应的count，目前按照java的来
            Pivot pivot = Pivot.newBuilder().setName("total_count").addMainIndexes(" ").setAggable(Boolean.TRUE)
                    .setFunc(FuncEnum.Func.COUNT).setMultiFunc(MultiFuncEnum.MultiFunc.UNSPECIFIED).build();
            int countResult = biGrpcClient.countData(worktableId, filters, countColumn, pivot);
            resultDict.put(name, countResult);
        });
        return resultDict;
    }

    //private int getPivotData(String date, String worktableId, String seqColId) {
    //    // 组装filter
    //    List<Filter> filters = new ArrayList<>();
    //    Filter timeColEndFilter = Filter.newBuilder().setCol("finish_time").setOption(OptionEnum.Option.GT)
    //            .addAllValues(Collections.singletonList(date))
    //            .setDataType(DataTypeEnum.DataType.DATETIME).build();
    //    Filter statusColEndFilter = Filter.newBuilder().setCol("status").setOption(OptionEnum.Option.EQ)
    //            .addAllValues(Collections.singletonList("成功完成"))
    //            .setDataType(DataTypeEnum.DataType.STRING)
    //            .build();
    //    filters.add(timeColEndFilter);
    //    filters.add(statusColEndFilter);
    //    // 组装NewColumn
    //    NewColumn countColumn = NewColumn.newBuilder()
    //            .setCol("total_count")
    //            .setName("total_count")
    //            .setColumnId(seqColId)
    //            .setDataType(DataTypeEnum.DataType.NUMBER)
    //            .setDateType(DateTypeEnum.DateType.TIME_Y).build();
    //    // python中Func是0对应的count， java中是1对应的count，目前按照java的来
    //    Pivot pivot = Pivot.newBuilder()
    //            .setName("total_count")
    //            .addAllMainIndexes(Collections.singletonList(" "))
    //            .addAllSubIndexes(new ArrayList<>())
    //            .setAggable(Boolean.TRUE)
    //            .setFunc(FuncEnum.Func.COUNT)
    //            .setMultiFunc(MultiFuncEnum.MultiFunc.UNSPECIFIED).build();
    //    return biGrpcClient.countData(worktableId, filters, countColumn, pivot);
    //}

    @Override
    public QdesProject createOneProject(QdesProject project, boolean forTemplet) {
        if (!forTemplet) {
            String creator = project.getCreator();
            if (StringUtils.isEmpty(creator)) {
                throw new BusinessException("creator is required");
            }
            List<String> groupIds = userGrpcClient.listAncestorIdsByUserId(creator);

            if (CollectionUtils.isNotEmpty(groupIds)) {
                project.setGroupList(JSONArray.toJSONString(groupIds));
            } else {
                project.setGroupList(JSONArray.toJSONString(new ArrayList<>()));
            }
        }
        int version = 1;
        project.setVersion(version);
        String orgId = project.getCompanyId();
        String code = getCode(orgId);
        project.setCode(code);

        String customAttr = project.getCustomAttr();
        // 没有指定customAttr的情况需要从全局设置里取配置
        JSONObject customAttrJs = getDefaultCustomAttr();
        customAttrJs.put("wxShare_title", project.getTitle());
        customAttrJs.put("wxCardShare_title", project.getTitle());
        if (StringUtils.isEmpty(customAttr)) {
            LambdaQueryWrapper<QdesProjectSetting> queryWrapper = Wrappers.lambdaQuery();
            queryWrapper.eq(QdesProjectSetting::getOrgId, orgId).last("limit 1");
            QdesProjectSetting projectsetting = qdesProjectsettingMapper.selectOne(queryWrapper);
            if (projectsetting != null) {
                customAttrJs.put(OPEN_GROUPING, projectsetting.getOpenGrouping());
                customAttrJs.put(OPEN_CUSTOM_CID, projectsetting.getOpenCustomCid());
            }
            project.setCustomAttr(JSONObject.toJSONString(customAttrJs));
        } else {
            JSONObject customA = JSONObject.parseObject(customAttr);
            customA.keySet().forEach(key -> customAttrJs.put(key, customA.get(key)));
            project.setCustomAttr(JSONObject.toJSONString(customAttrJs));
        }
        if (StringUtils.isEmpty(project.getGroupList())) {
            project.setGroupList("[]");
        }
        if (StringUtils.isEmpty(project.getWorktableInfo())) {
            project.setWorktableInfo("{}");
        }
        qdesProjectMapper.insert(project);
        if (!forTemplet) {
            CreateProjectDto projectDto = new CreateProjectDto().setId(project.getId()).setOrgId(orgId);
            qdesMessageSendService.qdesProjectCreatedSend(projectDto);
        }
        // 添加struct
        QdesQstruct qstruct = createStruct(project.getId(), project.getTitle());
        // 将structId更新到project中
        LambdaUpdateWrapper<QdesProject> updateWrapper = new LambdaUpdateWrapper<>();
        //构造查询条件-更新
        updateWrapper.set(QdesProject::getStructId, qstruct.getId())
                .eq(QdesProject::getId, project.getId())
                .eq(QdesProject::getVersion, 1);
        qdesProjectMapper.update(null, updateWrapper);
        project.setStructId(qstruct.getId());
        return project;
    }

    private QdesQstruct createStruct(String pid, String newTitle) {
        QdesQstruct newStruct = new QdesQstruct();
        newStruct.setProjectId(pid);
        newStruct.setVersion(1);
        newStruct.setSecretKey(RandomStringUtils.randomAlphanumeric(10));
        newStruct.setStatus(QSTRUCT_STATUS_DRAFT.getNumber());
        newStruct.setTemplates("[]");
        newStruct.setAttachments("[]");
        newStruct.setCsses("[]");
        newStruct.setGcss("{}");
        newStruct.setData("{}");
        newStruct.setUcss("{}");
        newStruct.setScores("{}");
        newStruct.setPolicy("[]");
        newStruct.setGjs("{}");
        newStruct.setScoreConfig("{}");
        qdesQstructMapper.insert(newStruct);
        return newStruct;
    }

    private String getCode(String orgId) {
        int maxCodeNum = qdesProjectMapper.getMaxCodeNum(QdesConstant.CODE_PREFIX.length() + 1, orgId);
        return QdesConstant.CODE_PREFIX + (maxCodeNum + 1);
    }

    /**
     * 默认自定义属性
     *
     * @return 数据
     */
    @NotNull
    private JSONObject getDefaultCustomAttr() {
        String attr = ResourceUtil.readUtf8Str(CUSTOM_ATTR_FILE_PATH);
        //String urlPrefix = StrUtil.removeSuffix(ossService.getCdn() + ossService.getFilePrefix(), "/");
        String urlPrefix = StrUtil.removeSuffix(storageService.getPropertiesValueBy(null, "domain") + storageService.getPropertiesValueBy(null, "base-path"), "/");
        return JSON.parseObject(String.format(attr, urlPrefix, urlPrefix));
        //String attr = ResourceUtil.readUtf8Str(CUSTOM_ATTR_FILE_PATH);
        //return JSON.parseObject(attr);
        /*JSONObject customAttr = new JSONObject();
        customAttr.put("ipOnce", "false");
        customAttr.put("devOnce", "false");
        customAttr.put("time_flag", "false");
        customAttr.put("nums_flag", "false");
        customAttr.put("begin_time", "");
        customAttr.put("end_time", "");
        customAttr.put("sample_num", "");
        customAttr.put("wxOnce", "false"); // 微信 答题次数限制开启
        customAttr.put("wxOnce_time", "90_days"); // 微信 控制答题的时间范围 只能、每小时、每天。。。 同ip、设备限制相同
        customAttr.put("wxOnce_num", 1); // 微信 控制答题次数
        customAttr.put("wxOnly", "false"); // 只能通过微信答题
        customAttr.put("wxShare", "true"); // 允许微信右上角分享
        customAttr.put("wxShare_img", "https://rs0.bestcem.cn/prod/rs/survey/logo0515.jpg"); // 自定义微信分享图标
        customAttr.put("wxShare_title", ""); // 自定义微信分享标题
        customAttr.put("wxCardShare_bg_img", ""); // 问卷分享卡片背景图
        customAttr.put("wxCardShare_title", ""); // 问卷分享卡片标题
        customAttr.put("wxShare_desc", "感谢您能抽出几分钟时间来填写以下内容，现在我们就马上开始吧！"); // 自定义微信分享描述
        customAttr.put("wxInfo_record", "false"); // 授权给公众号
        customAttr.put("wxInfo_collection", 0); // 微信信息收集方式
        customAttr.put("wxAuth_CEM", 0); // 通过倍市得官方微信服务号来收集信息或投放问卷
        customAttr.put("wxAuth_Custom", ""); // 我的微信认证服务号，最多授权四个
        customAttr.put("wxShare_qrcode_img", ""); // 微信分享二维码图片
        customAttr.put("wxShare_qrcode_img_id", ""); // 微信分享二维码id
        return customAttr;*/
    }

    @Override
    public XmResultJson<Object> getProjectExtParams(String projectId) {
        List<String> extParamsIdList = new ArrayList<>();
        QdesProject project = this.getById(projectId);
        if (Objects.isNull(project)) {
            return XmResultJson.fail(201, "Project[id={" + projectId + "}] not found");
        }
        LambdaQueryWrapper<QdesQstruct> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(QdesQstruct::getProjectId, projectId)
                .eq(QdesQstruct::getVersion, project.getVersion())
                .last("limit 1");
        QdesQstruct qdesQstruct = qdesQstructMapper.selectOne(queryWrapper);
        if (Objects.isNull(qdesQstruct)) {
            return XmResultJson.success(extParamsIdList);
        }
        JSONObject data = JSONObject.parseObject(qdesQstruct.getData());
        //"ext_params":{A:[1,2,3],B:[2,3,4]} 字母是外部参数id
        Optional.ofNullable(data)
                .map(it -> it.getJSONObject("ext_params"))
                .ifPresent(extParams -> {
                    extParamsIdList.addAll(extParams.keySet());
                    /*for (Map.Entry<String, Object> extParamsJsonentry : extParams.entrySet()) {
                        extParamsIdList.add(extParamsJsonentry.getKey());
                    }*/
                });

        return XmResultJson.success(extParamsIdList);
    }

    private String getAdminRoleId(String orgId) {
        User user = userGrpcClient.getAdminUserByOrgId(orgId);
        return Objects.nonNull(user) ? user.getId() : "";
    }

    private void setPermission(QdesProjectEntry projectentry, String userId) {
        if (projectentry == null) {
            return;
        }
        int DeliverTask = DELIVER_TASK.getValue();
        // 审核者应该有的权限
        if (userId.equals(projectentry.getUserId())) {
            String permissionArray = projectentry.getPermissionsArry();
            if (StringUtils.isEmpty(permissionArray)) {
                List<Integer> permissions = new ArrayList<>();
                permissions.add(DeliverTask);
                projectentry.setPermissionsArry(JSONObject.toJSONString(permissions));
            } else {
                List<Integer> permissions = JSONObject.parseArray(permissionArray, Integer.class);
                if (!permissions.contains(DeliverTask)) {
                    permissions.add(DeliverTask);
                }
                projectentry.setPermissionsArry(JSONObject.toJSONString(permissions));
            }
        } else {
            projectentry.setUserId(userId);
            List<Integer> permissions = new ArrayList<>();
            permissions.add(DeliverTask);
            projectentry.setPermissionsArry(JSONObject.toJSONString(permissions));
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer getProjectMaxGid(String id) {
        return getMaxGid(id);
    }

    public Integer getMaxGid(String id) {
        Integer maxGid = serviceSequenceService.getCurrentSequence("proj_gid_" + id);
        if (Objects.isNull(maxGid)) {
            maxGid = 0;
            QdesProject project = qdesProjectMapper.selectById(id);
            if (Objects.nonNull(project)) {
                QdesQstruct qdesQstruct = qdesQstructMapper.selectById(project.getStructId());
                if (Objects.nonNull(qdesQstruct)) {
                    maxGid = (Integer) JSONObject.parseObject(qdesQstruct.getData()).getOrDefault("max_gid", 0);
                    if (maxGid != 0) {
                        Integer sequence = serviceSequenceService.getSequence("proj_gid_" + id, maxGid);
                        if (sequence > 0) {
                            return sequence;
                        }
                    }
                }
            }
        }
        return maxGid;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public XmResultJson<?> setProjectMaxGid(String id, Integer maxGid) {
        //获当前问卷最大的gid
        Integer curMaxGid = getMaxGid(id);
        if (curMaxGid > maxGid) {
            return XmResultJson.fail(6, "gid[{" + curMaxGid + "}] cannot be replaced with {" + maxGid + "}");
        }
        if (maxGid > curMaxGid) {
            return XmResultJson.success(serviceSequenceService.getSequence("proj_gid_" + id, maxGid - curMaxGid));
        }
        return XmResultJson.success(curMaxGid);
    }

    @Override
    public List<JSONObject> getReleasedQuestions(QdesProject project, QdesQstruct qdesQstruct) {
        // 获取questions
        LambdaQueryWrapper<QdesQuestion> questionWrapper = Wrappers.lambdaQuery();
        questionWrapper.eq(QdesQuestion::getStructId, qdesQstruct.getId());
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        fillQstructData(qdesQstruct, qdesQuestions);

        Map<Integer, JSONObject> qdict = qdesQuestions.stream().collect(Collectors.toMap(QdesQuestion::getGid, self -> {
            JSONObject dataobj = JSONObject.parseObject(self.getData());
            String qtype = dataobj.getString("qtype");
            if (!dataobj.containsKey("custom_qtype")) {
                dataobj.put("custom_qtype", qtype);
            }
            String customqtype = dataobj.getString("custom_qtype");
            if ("score".equals(qtype)) {
                if ("star".equals(customqtype)) {
                    dataobj.put("custom_qtype", "star_score");
                }
                if ("cursor".equals(customqtype)) {
                    dataobj.put("custom_qtype", "cursor_score");
                }
            }
            return dataobj;
        }));

        JSONObject dataJson = JSONObject.parseObject(qdesQstruct.getData());
        List<JSONObject> questions = new ArrayList<>();
        List<JSONObject> selectqs = walkQstructQuestions(dataJson);
        selectqs.forEach(item -> {
            JSONObject q = qdict.get(item.getInteger("gid"));
            if (Objects.nonNull(q)) {
                //item.keySet().forEach(key -> {
                //    q.put(key, item.get(key));
                //});
                q.putAll(item);

                String cid = q.getOrDefault("cid", "").toString();
                q.put("_index", cid);
                q.put("cid", cid);
                questions.add(q);
            } else if (StrUtil.isNotBlank(item.getString("qtype"))) {
                log.error("兼容旧数据，旧数据题目信息在struct里");
                //兼容旧数据，旧数据题目信息在struct里
                //这里应该走不到
                String index = item.getString("_index");
                if (StrUtil.isNotBlank(index)) {
                    item.put("cid", index);
                }
                questions.add(item);
            }
        });
        return questions;
    }

    @Override
    //@Transactional(rollbackFor = Exception.class)
    public Map<String, Object> releaseProject(QdesProject project, QdesQstruct struct, Boolean force) {
        Integer old_status = project.getStatus();
        //手动开启事务
        //TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        try {
            //bug可能导致版本不一致
            if (!struct.getVersion().equals(project.getVersion())) {
                log.error("bug可能导致版本不一致");
                // 修改entery中的title
                LambdaUpdateWrapper<QdesProject> updateWrapper = new LambdaUpdateWrapper<>();
                //构造查询条件-更新
                updateWrapper
                        .set(QdesProject::getVersion, struct.getVersion())
                        .eq(QdesProject::getVersion, project.getVersion())
                        .eq(QdesProject::getId, project.getId());
                baseMapper.update(null, updateWrapper);
                project.setVersion(struct.getVersion());
            }
            Integer old_rel_version = project.getReleaseVer();
            if (!force) {
                //敏感词检测，调用百度api
                //await project_content_detect(session, qstruct, proj)
                projectContentDetect(struct, project);
            }

            if (project.getStatus().equals(PROJECT_STATUS_CREATED.getNumber()) || project.getStatus().equals(PROJECT_STATUS_FINISHED.getNumber())) {
                // 修改status
                LambdaUpdateWrapper<QdesProject> upw = new LambdaUpdateWrapper<>();
                upw.set(QdesProject::getStatus, PROJECT_STATUS_ACTIVATED.getNumber())
                        .set(QdesProject::getUpdatedDt, LocalDateTime.now())
                        .eq(QdesProject::getId, project.getId());
                baseMapper.update(null, upw);
                //project.setStatus(PROJECT_STATUS_ACTIVATED.getNumber());

                //修改projectentry status
                LambdaUpdateWrapper<QdesProjectEntry> uy = new LambdaUpdateWrapper<>();
                uy.set(QdesProjectEntry::getStatus, PROJECT_STATUS_ACTIVATED.getNumber())
                        .set(QdesProjectEntry::getUpdatedDt, LocalDateTime.now())
                        .eq(QdesProjectEntry::getProjectId, project.getId());
                projectentryMapper.update(null, uy);

                //await delete_project_cache(pid)
                //删除问卷缓存
                //redisService.del("survey_get_project#" + project.getId());

                Map<String, Object> mqdata = new HashMap<>();
                mqdata.put("project_id", project.getId());
                mqdata.put("prev", old_status);
                mqdata.put("current", PROJECT_STATUS_ACTIVATED.getNumber());
                StatusDto dto = objectMapper.convertValue(mqdata, StatusDto.class);

                qdesMessageSendService.projectStatusChangedSend(dto);
                qdesMessageSendService.projectUpdatedSend(dto);
            } else if (!project.getStatus().equals(PROJECT_STATUS_ACTIVATED.getNumber())) {
                throw new BusinessException("project status is not Created or Activated");
            }

            // 重设project.groupList
            String gs = project.getGroupList();
            if (StrUtil.isEmptyIfStr(gs) || "[]".equals(gs)) {
                List<String> groupIds = userGrpcClient.listAncestorIdsByUserId(SecurityContextHolder.getUserId());
                if (CollectionUtils.isNotEmpty(groupIds)) {
                    project.setGroupList(JSONArray.toJSONString(groupIds));
                    LambdaUpdateWrapper<QdesProject> upq = new LambdaUpdateWrapper<>();
                    upq.set(QdesProject::getUpdatedDt, LocalDateTime.now())
                            .set(QdesProject::getGroupList, JSONArray.toJSONString(groupIds))
                            .eq(QdesProject::getId, project.getId());
                    baseMapper.update(null, upq);
                }
            }

            String link = doReleaseProject(project, struct, null);
            StatusDto dto = new StatusDto();
            dto.setProjectId(project.getId());
            qdesMessageSendService.projectUpdatedSend(dto);

            if (old_rel_version.equals(0)) {
                log.info("第一次发布");
                //第一次发布 qdes.project_first_released
                JSONObject mqdto = new JSONObject();
                mqdto.put("org_id", project.getCompanyId());
                mqdto.put("project_id", project.getId());
                mqdto.put("user_id", SecurityContextHolder.getUserId());
                qdesMessageSendService.projectFirstReleasedSend(mqdto);
                //调用rpc创建默认投放
                //deliverGrpcClient.createDefaultDeliverBase(project.getId(), SecurityContextHolder.getUserId(), SecurityContextHolder.getOrgId());
                //deliverGrpcClient.createDefaultDeliverStore(project.getId(), SecurityContextHolder.getUserId(), SecurityContextHolder.getOrgId());
                CompletableFuture.runAsync(() -> deliverGrpcClient.createDefaultDeliverBase(project.getId(), SecurityContextHolder.getUserId(), SecurityContextHolder.getOrgId()), releaseProjectTaskExecutor);
                CompletableFuture.runAsync(() -> deliverGrpcClient.createDefaultDeliverStore(project.getId(), SecurityContextHolder.getUserId(), SecurityContextHolder.getOrgId()), releaseProjectTaskExecutor);
            }

            JSONObject mqdto = new JSONObject();
            mqdto.put("org_id", project.getCompanyId());
            mqdto.put("project_id", project.getId());
            mqdto.put("version", null);
            mqdto.put("user_id", SecurityContextHolder.getUserId());
            qdesMessageSendService.projectReleasedSend(mqdto);

            //await delete_project_cache(pid)
            //删除问卷缓存
            //redisService.del("survey_get_project#" + project.getId());

            //删除敏感题
            delSensitive(project);

            //手动提交事务
            //dataSourceTransactionManager.commit(transactionStatus);//提交
            Map<String, Object> retmap = new HashMap<>();
            retmap.put("link", link);
            retmap.put("draft_id", project.getStructId());
            retmap.put("version", project.getVersion());
            retmap.put("releaseVer", project.getReleaseVer());
            return retmap;
        } catch (Exception e) {
            log.error("releaseProject error", e);
            //回滚
            //dataSourceTransactionManager.rollback(transactionStatus);
        }
        return null;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public XmResultJson<QdesImportProjectVo> importProject(ProjectImportParam param, MultipartFile file) {
        log.info("Now to importProject, param = {}", param);
        QdesImportProjectVo projectVo = new QdesImportProjectVo();
        String userId = param.getUserId();
        String orgId = param.getOrgId();
        String type = param.getType();
        if (!"txt".equals(type) && !"excel".equals(type)) {
            throw new BusinessException("type is invaild");
        }
        QdesProject newProject = new QdesProject();
        newProject.setCreator(userId);
        newProject.setCompanyId(orgId);
        JSONObject attr = new JSONObject();
        attr.put("ftype", type);
        attr.put("fstatus", "pre");

        // 从全局设置里取配置
        LambdaQueryWrapper<QdesProjectSetting> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesProjectSetting::getOrgId, orgId)
                .last("limit 1");
        QdesProjectSetting projectsetting = qdesProjectsettingMapper.selectOne(queryWrapper);
        if (projectsetting != null) {
            attr.put("openGrouping", projectsetting.getOpenGrouping());
            attr.put("openCustomCid", projectsetting.getOpenCustomCid());
        }
        newProject.setCustomAttr(attr.toJSONString());
        newProject.setTitle("");
        newProject.setStatus(PROJECT_STATUS_CREATED.getNumber());
        newProject.setPtype(PROJECT_PTYPE_SURVEY.getNumber());
        newProject.setReleaseVer(0);
        newProject.setIsCopyDemo(false);
        if ("txt".equals(type)) {
            JSONObject txtData = param.getData();
            if (txtData == null || txtData.isEmpty()) {
                throw new BusinessException("data is required");
            }
            QdesProject project = createOneProject(newProject, true);
            QdesQstruct qdesQstruct = qdesQstructMapper.selectById(project.getStructId());
            JSONObject paramObj = new JSONObject();
            paramObj.put("data", txtData);
            qdesQstructService.updateQstruct(qdesQstruct, paramObj, project, false, true);
            projectVo.setPid(project.getId());
            projectVo.setSid(qdesQstruct.getId());
        } else {
            if (file == null || file.isEmpty()) {
                throw new BusinessException("未上传文件或者文件为空！");
            }
            String fileName = file.getOriginalFilename();
            String fileType = FileUtil.getFileExtension(fileName);
            if (!"xls".equals(fileType) && !"xlsx".equals(fileType)) {
                throw new BusinessException("Upload Project File is not a excel");
            }
            return importProjectFromExcel(file, param, newProject);
        }
        return XmResultJson.success(projectVo);
    }

    public void delSensitive(QdesProject project) {
        // 获取struct
        LambdaQueryWrapper<QdesQstruct> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(QdesQstruct::getProjectId, project.getId())
                .eq(QdesQstruct::getVersion, project.getReleaseVer())
                .last("limit 1");
        QdesQstruct struct = qdesQstructMapper.selectOne(queryWrapper);
        if (Objects.isNull(struct)) {
            throw new BusinessException(String.format("delSensitive qstruct[projectID=%s,version=%s] not found", project.getId(), project.getReleaseVer()));
        }
        // 获取questions
        LambdaQueryWrapper<QdesQuestion> questionWrapper = Wrappers.lambdaQuery();
        questionWrapper.eq(QdesQuestion::getStructId, struct.getId());
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        fillQstructData(struct, qdesQuestions);

        //操作qdes_projectextraattr
        LambdaQueryWrapper<QdesProjectExtraAttr> qwr = Wrappers.lambdaQuery();
        qwr.eq(QdesProjectExtraAttr::getProjectId, struct.getProjectId()).last("limit 1");
        QdesProjectExtraAttr pextra = projectextraattrMapper.selectOne(qwr);
        if (!Objects.isNull(pextra) && StrUtil.isNotBlank(pextra.getCustomAttr())) {
            JSONObject customAttrobj = JSONObject.parseObject(pextra.getCustomAttr());
            JSONObject sensitive = customAttrobj.getJSONObject("sensitive");
            if (!sensitive.isEmpty()) {
                Set<String> sgids = sensitive.keySet();
                Set<String> allgids = new HashSet<>();

                JSONObject dataJson = JSONObject.parseObject(struct.getData());
                List<JSONObject> selectqs = walkQstructQuestions(dataJson);
                selectqs.forEach(q -> allgids.add(q.getString("gid")));
                log.info("---sgids={}, allgids={}", sgids, allgids);

                JSONObject newsensitive = new JSONObject();
                sgids.forEach(gid -> {
                    if (allgids.contains(gid)) {
                        newsensitive.put(gid, sensitive.getJSONObject(gid));
                    }
                });
                //更新customAttr.sensitive
                if (newsensitive.isEmpty()) {
                    customAttrobj.remove("sensitive");
                } else {
                    customAttrobj.put("sensitive", newsensitive);
                }

                LambdaUpdateWrapper<QdesProjectExtraAttr> upq = new LambdaUpdateWrapper<>();
                upq.set(QdesProjectExtraAttr::getUpdatedDt, LocalDateTime.now())
                        .set(QdesProjectExtraAttr::getCustomAttr, JSONObject.toJSONString(customAttrobj))
                        .eq(QdesProjectExtraAttr::getId, pextra.getId());
                projectextraattrMapper.update(null, upq);
            }
        }
    }

    public String doReleaseProject(QdesProject project, QdesQstruct struct, Integer version) throws Exception {
        String pid = project.getId();
        Integer releaseVer = version == null ? project.getVersion() : version;
        boolean sver = needVersionInc(project, struct);
        log.info("do_release_project version={}, need_version_inc={}", version, sver);
        if (version != null || sver) {
            log.info("准备publishProject");
            //await publish_project(session, pid, proj, releaseVer)
            publishProject(project, releaseVer);
            surveyGrpcClient.copyExtParam(pid, releaseVer);
            surveyGrpcClient.createColumnsRelsProject(pid, project.getCompanyId());
        } else {
            LambdaQueryWrapper<QdesQstruct> queryWrapper = Wrappers.lambdaQuery();
            queryWrapper.eq(QdesQstruct::getProjectId, pid)
                    .eq(QdesQstruct::getVersion, releaseVer)
                    .last("limit 1");
            QdesQstruct qstruct = qdesQstructMapper.selectOne(queryWrapper);

            if (qstruct.getStatus().equals(0)) {//兼容旧数据(草稿qstruct status字段不存在, 会导致一直显示可发布)
                LambdaUpdateWrapper<QdesQstruct> upq = new LambdaUpdateWrapper<>();
                upq.set(QdesQstruct::getUpdatedDt, LocalDateTime.now())
                        .set(QdesQstruct::getStatus, 1)
                        .eq(QdesQstruct::getId, qstruct.getId());
                qdesQstructMapper.update(null, upq);
            }

            releaseVer = project.getReleaseVer();
            queryWrapper.clear();
            queryWrapper.eq(QdesQstruct::getProjectId, project.getId())
                    .eq(QdesQstruct::getVersion, project.getReleaseVer())
                    .last("limit 1");
            QdesQstruct relqstruct = qdesQstructMapper.selectOne(queryWrapper);
            relqstruct.setWithdrawDt(null);
            relqstruct.setGcss(qstruct.getGcss());
            relqstruct.setTemplates(qstruct.getTemplates());
            relqstruct.setUcss(qdesLibprojectService.genQstructUcss(pid, qstruct));
            relqstruct.setGjs(qdesLibprojectService.genQstructGjs(pid, qstruct));
            relqstruct.setPolicy(qstruct.getPolicy());
            relqstruct.setAttachments(qstruct.getAttachments());
            relqstruct.setStatus(QSTRUCT_STATUS_RELEASE.getNumber());

            qdesQstructMapper.alwaysUpdateSomeColumnById(relqstruct);
        }
        return genReleaseProject(pid, releaseVer);
    }

    private void publishProject(QdesProject project, Integer releaseVer) throws IOException {
        Integer version = releaseVer + 1;
        LambdaUpdateWrapper<QdesProject> projectUpdateWrapper = new LambdaUpdateWrapper<>();
        projectUpdateWrapper.set(QdesProject::getUpdatedDt, LocalDateTime.now())
                .set(QdesProject::getVersion, version)
                .eq(QdesProject::getId, project.getId())
                .eq(QdesProject::getVersion, project.getVersion());
        baseMapper.update(null, projectUpdateWrapper);
        project.setVersion(version);

        qdesLibprojectService.copyStruct(project.getStructId(), null, null, false, version);

        log.info("releaseVer={},version={}", releaseVer, version);
        projectUpdateWrapper.clear();
        QdesProject projectUpdate = new QdesProject();
        projectUpdate.setUpdatedDt(LocalDateTime.now());
        projectUpdate.setReleaseVer(releaseVer);
        projectUpdateWrapper.eq(QdesProject::getId, project.getId())
                .eq(QdesProject::getVersion, version);
        baseMapper.update(projectUpdate, projectUpdateWrapper);
        project.setReleaseVer(releaseVer);

        LambdaQueryWrapper<QdesQstruct> qstructQueryWrapper = Wrappers.lambdaQuery();
        qstructQueryWrapper.eq(QdesQstruct::getProjectId, project.getId())
                .eq(QdesQstruct::getVersion, releaseVer)
                .last("limit 1");
        //log.info("releaseVer={}", releaseVer);
        QdesQstruct release_struct = qdesQstructMapper.selectOne(qstructQueryWrapper);
        release_struct.setReleaseDt(LocalDateTime.now());
        release_struct.setWithdrawDt(null);
        release_struct.setStatus(QSTRUCT_STATUS_RELEASE.getNumber());
        qdesQstructMapper.alwaysUpdateSomeColumnById(release_struct);

        qstructQueryWrapper.clear();
        qstructQueryWrapper.eq(QdesQstruct::getProjectId, project.getId())
                .eq(QdesQstruct::getVersion, version)
                .last("limit 1");
        //log.info("version={}", version);
        QdesQstruct oldest_struct = qdesQstructMapper.selectOne(qstructQueryWrapper);
        oldest_struct.setReleaseDt(null);
        oldest_struct.setWithdrawDt(null);
        oldest_struct.setStatus(QSTRUCT_STATUS_RELEASE.getNumber());
        qdesQstructMapper.alwaysUpdateSomeColumnById(oldest_struct);

        if (releaseVer > 1) {
            log.info("问卷发布记录，结束时间应该是下一次的发布时间");
            //问卷发布记录，结束时间应该是下一次的发布时间
            LambdaUpdateWrapper<QdesQstruct> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.set(QdesQstruct::getUpdatedDt, LocalDateTime.now())
                    .set(QdesQstruct::getWithdrawDt, LocalDateTime.now())
                    .eq(QdesQstruct::getProjectId, project.getId())
                    .eq(QdesQstruct::getVersion, releaseVer - 1);
            qdesQstructMapper.update(null, updateWrapper);

            qstructQueryWrapper.clear();
            qstructQueryWrapper.eq(QdesQstruct::getProjectId, project.getId())
                    .eq(QdesQstruct::getVersion, releaseVer - 1)
                    .last("limit 1");
            QdesQstruct old_qstruct = qdesQstructMapper.selectOne(qstructQueryWrapper);
            if (Objects.nonNull(old_qstruct)) {
                publishQuestionModified(old_qstruct, release_struct);
            }
        }

        //redisService.del("survey_get_project#" + project.getId());
    }

    private void publishQuestionModified(QdesQstruct old_qstruct, QdesQstruct qstruct) {
        JSONArray diff_qs = new JSONArray();
        Map<Integer, JSONObject> old_qids = qdesQuestionMapper.selectList(Wrappers.<QdesQuestion>lambdaQuery().eq(QdesQuestion::getStructId, old_qstruct.getId()))
                .stream().collect(Collectors.toMap(QdesQuestion::getGid, obj -> JSONObject.parseObject(JSON.toJSONString(obj))));

        List<QdesQuestion> list = qdesQuestionMapper.selectList(Wrappers.<QdesQuestion>lambdaQuery().eq(QdesQuestion::getStructId, qstruct.getId()));
        for (QdesQuestion item : list) {
            JSONObject _item_dict = JSONObject.parseObject(JSON.toJSONString(item));
            JSONObject old_q = old_qids.get(item.getGid());
            JSONObject qdata = JSONObject.parseObject(item.getData());
            JSONObject oldqdata = new JSONObject();
            if (!ObjectUtils.isEmpty(old_q)) {
                oldqdata = Optional.ofNullable(old_q.getJSONObject("data")).orElse(new JSONObject());
            }
            if (ObjectUtils.isEmpty(old_q)) {
                JSONObject tmp = new JSONObject();
                tmp.put("type", "create");
                tmp.put("data", qdata);
                diff_qs.add(tmp);
            } else if (!oldqdata.equals(qdata)) {
                JSONObject old_q_data = new JSONObject();
                old_q_data.putAll(oldqdata);

                JSONObject q_data = new JSONObject();
                q_data.putAll(qdata);

                JSONArray old_items_list = Optional.ofNullable(old_q_data.getJSONArray("items")).orElse(new JSONArray());
                JSONArray old_row_items_list = Optional.ofNullable(old_q_data.getJSONArray("row_items")).orElse(new JSONArray());

                JSONArray items_list = Optional.ofNullable(q_data.getJSONArray("items")).orElse(new JSONArray());
                JSONArray row_items_list = Optional.ofNullable(q_data.getJSONArray("row_items")).orElse(new JSONArray());

                HashMap<Integer, JSONObject> old_items = new HashMap<>();
                HashMap<Integer, JSONObject> old_rows = new HashMap<>();
                HashMap<Integer, JSONObject> items = new HashMap<>();
                HashMap<Integer, JSONObject> rows = new HashMap<>();
                old_items_list.forEach(obj -> {
                    JSONObject it = (JSONObject) obj;
                    old_items.put(it.getInteger("gid"), it);
                });
                old_row_items_list.forEach(obj -> {
                    JSONObject it = (JSONObject) obj;
                    old_rows.put(it.getInteger("gid"), it);
                });
                items_list.forEach(obj -> {
                    JSONObject it = (JSONObject) obj;
                    items.put(it.getInteger("gid"), it);
                });
                row_items_list.forEach(obj -> {
                    JSONObject it = (JSONObject) obj;
                    rows.put(it.getInteger("gid"), it);
                });

                Set<Integer> new_items = difference(items.keySet(), old_items.keySet());
                Set<Integer> del_items = difference(old_items.keySet(), items.keySet());
                Set<Integer> new_rows = difference(rows.keySet(), old_rows.keySet());
                Set<Integer> del_rows = difference(old_rows.keySet(), rows.keySet());

                //1.填空题-直接获取content_type
                Object old_content_type = old_q_data.get("content_type");
                Object content_type = q_data.get("content_type");

                //2.items内部open_attrs
                boolean is_items_open_attrs_changed = compareItemDiff(old_items_list, items_list);

                //3.rows_items内部open_attrs
                boolean is_row_items_open_attrs_changed = compareItemDiff(old_row_items_list, row_items_list);

                if (CollUtil.isNotEmpty(new_items)
                        || CollUtil.isNotEmpty(del_items)
                        || CollUtil.isNotEmpty(new_rows)
                        || CollUtil.isNotEmpty(del_rows)
                        || !Objects.equals(old_content_type, content_type)
                        || is_items_open_attrs_changed
                        || is_row_items_open_attrs_changed) {
                    JSONObject data = new JSONObject();
                    data.put("gid", old_q.get("gid"));
                    data.put("old_qtype", old_q.getJSONObject("data").get("qtype"));

                    JSONArray tmp1 = new JSONArray();
                    new_items.forEach(i -> tmp1.add(items.get(i)));
                    data.put("new_items", tmp1);

                    JSONArray tmp2 = new JSONArray();
                    del_items.forEach(i -> tmp2.add(old_items.get(i)));
                    data.put("del_items", tmp2);

                    JSONArray tmp3 = new JSONArray();
                    new_rows.forEach(i -> tmp3.add(rows.get(i)));
                    data.put("new_rows", tmp3);

                    JSONArray tmp4 = new JSONArray();
                    del_rows.forEach(i -> tmp4.add(old_rows.get(i)));
                    data.put("del_rows", tmp4);

                    JSONObject tmp = new JSONObject();
                    tmp.put("type", "update");
                    tmp.put("data", data);
                    diff_qs.add(tmp);
                }
            }
            old_qids.remove(_item_dict.getInteger("gid"));
        }

        old_qids.keySet().forEach(gid -> {
            JSONObject tmp = new JSONObject();
            tmp.put("type", "delete");
            tmp.put("gid", gid);
            diff_qs.add(tmp);
        });

        if (!ObjectUtils.isEmpty(diff_qs)) {
            JSONObject mqdata = new JSONObject();
            mqdata.put("project_id", qstruct.getProjectId());
            mqdata.put("version", qstruct.getVersion());
            mqdata.put("data", diff_qs);
            qdesMessageSendService.projectQuestionsChangedSend(mqdata);
        }
    }

    private boolean compareItemDiff(JSONArray old_items_list, JSONArray new_items_list) {
        List<JSONObject> old_open_attrs_list = new ArrayList<>();
        List<JSONObject> new_open_attrs_list = new ArrayList<>();
        old_items_list.forEach(tmp -> {
            JSONObject item = (JSONObject) tmp;
            JSONObject open_attrs = Optional.ofNullable(item.getJSONObject("open_attrs")).orElse(new JSONObject());
            open_attrs.put("gid", item.get("gid"));
            old_open_attrs_list.add(open_attrs);
        });

        new_items_list.forEach(tmp -> {
            JSONObject item = (JSONObject) tmp;
            JSONObject open_attrs = Optional.ofNullable(item.getJSONObject("open_attrs")).orElse(new JSONObject());
            open_attrs.put("gid", item.get("gid"));
            new_open_attrs_list.add(open_attrs);
        });

        return !compare(old_open_attrs_list, new_open_attrs_list);
    }

    private boolean compare(List<JSONObject> list1, List<JSONObject> list2) {
        if (list1.size() != list2.size()) {
            return false;
        }
        list1.sort(Comparator.comparingInt(o -> o.getIntValue("gid")));
        list2.sort(Comparator.comparingInt(o -> o.getIntValue("gid")));
        for (int i = 0; i < list1.size(); i++) {
            JSONObject obj1 = list1.get(i);
            JSONObject obj2 = list2.get(i);
            if (!Objects.equals(obj1, obj2)) {
                return false;
            }
        }
        return true;
    }

    private Set<Integer> difference(Set<Integer> listA, Set<Integer> listB) {
        listA.removeAll(listB);
        return new HashSet<>(listA);
    }

    private String genReleaseProject(String project_id, Integer releaseVer) throws IOException {
        QdesProject qdesProject = getReleasedProject(project_id, releaseVer);
        if (ObjectUtil.isEmpty(qdesProject)) {
            return "";
        }
        log.info("genReleaseProject qdesProject={}", JSON.toJSONString(qdesProject));
        QdesProjectDto qdesProjectDto = new QdesProjectDto();
        BeanUtil.copyProperties(qdesProject, qdesProjectDto);
        JSONObject json = JSONObject.parseObject(JSON.toJSONString(qdesProjectDto, SerializerFeature.WriteMapNullValue));
        log.info("genReleaseProject json={}", JSON.toJSONString(json));
        if (json.containsKey("qstruct")) {
            json.put("project", parseProject(json.getJSONObject("qstruct").getJSONObject("data"), json.getJSONObject("customAttr")));
            json.put("gcss", json.getJSONObject("qstruct").get("gcss"));
            json.put("ucss", json.getJSONObject("qstruct").get("ucss"));
            json.put("gjs", json.getJSONObject("qstruct").get("gjs"));
            json.put("templates", json.getJSONObject("qstruct").get("templates"));
            json.put("policy", json.getJSONObject("qstruct").get("policy"));
            json.put("attachments", json.getJSONObject("qstruct").get("attachments"));
        }
        if (json.containsKey("qlogic")) {
            json.put("qlogic", json.getJSONObject("qlogic").get("data"));
        }
        json.remove("qstruct");
        JSONObject result = new JSONObject();
        result.put("data", json);
        String filename = "project.js";
        String resultString = JSON.toJSONString(result, SerializerFeature.WriteMapNullValue);
        log.info("genReleaseProject resultString={}", resultString);
        String path = ROOT_FILE_PATH + File.separator + StrUtil.replace(OssPathEnum.TEMP_QDES.getPath(), "/", File.separator);
        File fileInfo = cn.hutool.core.io.FileUtil.touch(path, filename);
        cn.hutool.core.io.FileUtil.writeUtf8String(resultString, fileInfo);

        //上传文件
        //String url = ossService.uploadSimpleFile2(fileInfo, filename, QdesConstant.getSurveyProjectJsPathV2(project_id), 0, null, "", OssPathEnum.SURVEY);
        String url = storageService.upload(fileInfo, filename, QdesConstant.getSurveyProjectJsPathV2(project_id), new Dict()).getUrl();
        return QdesConstant.getSurveyProjectJsPathV2(project_id) + filename;
    }

    public boolean needVersionInc(QdesProject project, QdesQstruct struct) {
        // 获取questions
        LambdaQueryWrapper<QdesQuestion> questionWrapper = Wrappers.lambdaQuery();
        questionWrapper.eq(QdesQuestion::getStructId, struct.getId());
        List<QdesQuestion> qdesQuestions = qdesQuestionMapper.selectList(questionWrapper);
        fillQstructData(struct, qdesQuestions);
        Integer version = struct.getVersion();
        if (version < 2) {
            log.info("needVersionInc version < 2");
            return true;
        }
        Integer lastversion = project.getReleaseVer();
        if (lastversion.equals(0)) {
            lastversion = version - 1;
        }

        // 获取last_qstruct
        LambdaQueryWrapper<QdesQstruct> qw = Wrappers.lambdaQuery();
        qw.eq(QdesQstruct::getProjectId, project.getId())
                .eq(QdesQstruct::getVersion, lastversion)
                .last("limit 1");
        QdesQstruct lastqstruct = qdesQstructMapper.selectOne(qw);

        // 获取questions
        LambdaQueryWrapper<QdesQuestion> qwq = Wrappers.lambdaQuery();
        qwq.eq(QdesQuestion::getStructId, lastqstruct.getId());
        List<QdesQuestion> qs = qdesQuestionMapper.selectList(qwq);
        fillQstructData(lastqstruct, qs);

        JSONObject o1 = JSONObject.parseObject(struct.getData());
        JSONObject o2 = JSONObject.parseObject(lastqstruct.getData());
        log.info("needVersionInc o1={}", o1.toJSONString());
        log.info("needVersionInc o2={}", o2.toJSONString());
        if (!o1.equals(o2)) {
            log.info("needVersionInc o1不等于o2");
            return true;
        }

        JSONObject o3 = JSONObject.parseObject(struct.getScores());
        JSONObject o4 = JSONObject.parseObject(lastqstruct.getScores());
        log.info("needVersionInc o3={}", o3.toJSONString());
        log.info("needVersionInc o4={}", o4.toJSONString());
        if (!o3.equals(o4)) {
            log.info("needVersionInc o3不等于o4");
            return true;
        }
        return false;
    }

    public void projectContentDetect(QdesQstruct struct, QdesProject project) {
        // 筛选文字结果
        Set<String> contents = new HashSet<>(Collections.singletonList(project.getTitle()));
        picktTextFromJsonStr(contents, struct.getData(), "", QS_KEYS);

        LambdaQueryWrapper<QdesQuestion> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(QdesQuestion::getStructId, struct.getId());
        List<QdesQuestion> questionList = qdesQuestionMapper.selectList(wrapper);
        Map<Integer, JSONObject> allqs = new HashMap<>();
        questionList.stream().filter(q -> !q.getDeleted()).forEach(question -> allqs.put(question.getGid(), JSONObject.parseObject(question.getData())));

        JSONArray questions = new JSONArray();
        //questions = get_qstruct_questions(params['data'], all_qids)
        // 符合条件的question.data集合
        questionService.getQstructQuestions(JSON.parseObject(struct.getData()), allqs, questions);
        picktTextFromJsonStr(contents, JSONArray.toJSONString(questions), "", Q_KEYS);

        //处理groups
        JSONObject qdata = JSONObject.parseObject(struct.getData());
        if (Objects.nonNull(qdata.get("groups"))) {
            JSONObject groups = qdata.getJSONObject("groups");
            groups.forEach((k, v) -> {
                JSONObject vi = (JSONObject) v;
                if (Objects.nonNull(vi.get("data"))) {
                    JSONArray array = vi.getJSONArray("data");
                    array.forEach(it -> {
                        JSONObject oiu = (JSONObject) it;
                        String ts = HtmlUtil.cleanHtmlTag(oiu.getString("title"));
                        if (StrUtil.isNotBlank(ts)) {
                            contents.add(ts);
                        }
                    });
                }
            });
        }

        if (CollectionUtils.isNotEmpty(contents)) {
            //TODO 调用百度接口，过滤敏感词
        }
    }

    public void picktTextFromJsonStr(Set<String> contents, String data, String prekey, Set<String> kys) {
        if (JSONUtil.isTypeJSONObject(data)) {
            JSONObject obj = JSONObject.parseObject(data);
            if (!obj.isEmpty()) {
                obj.forEach((key, value) -> {
                    String newprekey = StrUtil.isNotBlank(prekey) ? prekey.concat(".").concat(key) : key;
                    if (JSONUtil.isTypeJSON(value.toString())) {
                        picktTextFromJsonStr(contents, JSON.toJSONString(value), newprekey, kys);
                    } else {
                        picktTextFromJsonStr(contents, value.toString(), newprekey, kys);
                    }
                });
            }
        } else if (JSONUtil.isTypeJSONArray(data)) {
            JSONArray array = JSONArray.parseArray(data);
            array.forEach(it -> picktTextFromJsonStr(contents, JSON.toJSONString(it), prekey, kys));
        } else if (kys.contains(prekey)) {
            String ts = HtmlUtil.cleanHtmlTag(data);
            if (StrUtil.isNotBlank(ts)) {
                contents.add(ts);
            }
        }
    }


    /**
     * 这就是qstruct.data.item 用question.data填充，且里面加入id:（question.id）
     *
     * @param qdesQstruct
     * @param qdesQuestions
     * @author chenglong.yue <chenglong.yue@idiaoyan.com>
     * @date 2022/9/27 18:22
     */
    @Override
    public void fillQstructData(QdesQstruct qdesQstruct, List<QdesQuestion> qdesQuestions) {
        String sData = qdesQstruct.getData();
        if (StringUtils.isNotEmpty(sData)) {
            Map<Integer, String> questions = new HashMap<>();
            qdesQuestions.forEach(q -> {
                JSONObject dataJson = new JSONObject();
                String data = q.getData();
                if (StringUtils.isEmpty(data)) {
                    dataJson.put("id", q.getId());
                } else {
                    dataJson = JSONObject.parseObject(data);
                    dataJson.put("id", q.getId());
                }
                questions.put(q.getGid(), JSONObject.toJSONString(dataJson));
            });
            qdesQstruct.setData(parseItem(questions, sData));
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void readAllProject(String userId, String orgId) {
        // 一键清除所有未读
        LambdaQueryWrapper<QdesProjectEntry> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(QdesProjectEntry::getUserId, userId);
        queryWrapper.select(QdesProjectEntry::getId);
        List<Object> idsObj = projectentryMapper.selectObjs(queryWrapper);
        if (CollectionUtils.isNotEmpty(idsObj)) {
            List<String> ids = idsObj.stream().map(Object::toString).collect(Collectors.toList());
            LambdaUpdateWrapper<QdesProjectEntry> upq = new LambdaUpdateWrapper<>();
            upq.set(QdesProjectEntry::getBadgeStatus, PROJECTENTRY_BADGE_STATUS_READ.getValue())
                    .set(QdesProjectEntry::getUpdatedDt, LocalDateTime.now())
                    .in(QdesProjectEntry::getId, ids);
            projectentryMapper.update(null, upq);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public XmResultJson<JSONObject> putAttr(String projectId, JSONObject attrJson) throws IOException {
        String orgId = SecurityContextHolder.getOrgId();
        //1.先把前端传的json放进map
        Map<String, Object> userMap = new HashMap<>(attrJson);
        if ("1".equals(userMap.get("openGrouping"))) {
            return XmResultJson.fail(-1, "分组模式已下线，问卷不能切换为分组模式！");
        }
        if (ObjectUtil.isNotNull(userMap.get("wxShare_title")) && userMap.get("wxShare_title").toString().length() > 64) {
            return XmResultJson.fail(-1, "微信分享标题超过最大长度限制");
        }
        if (ObjectUtil.isNotNull(userMap.get("wxShare_desc")) && userMap.get("wxShare_desc").toString().length() > 120) {
            return XmResultJson.fail(-1, "微信分享标描述超过最大长度限制");
        }
        if (ObjectUtil.isNotNull(userMap.get("wxCardShare_title")) && userMap.get("wxCardShare_title").toString().length() > 64) {
            return XmResultJson.fail(-1, "问卷卡片分享标题超过最大长度限制");
        }
        //int count = (int) getFinishedRspdCount(projectId, RESPONDENT_DATA_STATUS_FORMAL.getNumber(), RESPONDENT_STATUS_FINISH.getNumber()).getOrDefault("count", 0);
        Collection<Integer> countList = getFinishedRspdCountV2(projectId, RESPONDENT_DATA_STATUS_FORMAL.getNumber(), RESPONDENT_STATUS_FINISH.getNumber()).get("count");
        Integer count = countList.iterator().next();
        if (count == null || count == 0) {
            if (ObjectUtil.isNotNull(userMap.get("ipOnce")) && Boolean.parseBoolean(userMap.get("ipOnce").toString())) {
                String key = String.format(SURVEY_IP_CNT_FORMAT_KEY, orgId.substring(orgId.length() - 4), projectId);
                //key = f"Survey:{org_id[-4:]}:{pid}:ip-cnt"
                redisService.set(key, 1);
            }
        }
        //await survey_get_project.bust(pid)  # 删除project缓存，为了优化性能survey模块获取project方法做了缓存，此处需要删除缓存

        if (ObjectUtil.isNotNull(userMap.get("openGrouping")) && ObjectUtil.isNotNull(userMap.get("openCustomCid"))) {
            LambdaUpdateWrapper<QdesProjectSetting> upq = new LambdaUpdateWrapper<>();
            upq.set(QdesProjectSetting::getOpenGrouping, userMap.get("openGrouping"))
                    .set(QdesProjectSetting::getOpenCustomCid, userMap.get("openCustomCid"))
                    .eq(QdesProjectSetting::getOrgId, orgId);
            LambdaQueryWrapper<QdesProjectSetting> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(QdesProjectSetting::getOrgId, orgId);
            if (qdesProjectsettingMapper.exists(queryWrapper)) {
                qdesProjectsettingMapper.update(null, upq);
            } else {
                QdesProjectSetting qdesProjectsetting = new QdesProjectSetting();
                qdesProjectsetting.setOrgId(orgId);
                qdesProjectsetting.setOpenCustomCid((Integer) userMap.get("openCustomCid"));
                qdesProjectsetting.setOpenGrouping((Integer) userMap.get("openGrouping"));
                qdesProjectsettingMapper.insert(qdesProjectsetting);
            }
        }


        QdesProject qdesProject = baseMapper.selectById(projectId);
        JSONObject customAttr = JSONObject.parseObject(qdesProject.getCustomAttr());
        //保留系统中在参数中没有的key
        for (Map.Entry<String, Object> map : customAttr.entrySet()) {
            if (!userMap.containsKey(map.getKey())) {
                userMap.put(map.getKey(), map.getValue());
            }
        }
        //items更新pGroupGid
        resetQstructByOpenGrouping(qdesProject.getStructId(), attrJson, customAttr, qdesProject);

        qdesProject.setCustomAttr(JSON.toJSONString(userMap));

        //更新问卷状态定时器
        if (Boolean.parseBoolean(userMap.get("time_flag").toString())) {
            Date date = new Date();
            if ((long) userMap.get("end_time") > date.getTime()) {
                qdesProject.setTimerExpire(Integer.parseInt(String.valueOf((long) userMap.get("end_time") / 1000)));
            } else {
                qdesProject.setTimerExpire(null);
            }
        } else {
            qdesProject.setTimerExpire(null);
        }
        //更新qdesProject
        baseMapper.alwaysUpdateSomeColumnById(qdesProject);
        //redisService.del("survey_get_project#" + projectId);

        if (userMap.containsKey("devOnce") || userMap.containsKey("customTipText")) {
            genReleaseProject(projectId, qdesProject.getReleaseVer());
        }

        return XmResultJson.success(new JSONObject());
    }

    @Override
    public JSONObject getFinishedRspdCount(String projectId, Integer dataStatus, Integer status) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("project_id", projectId);
        jsonObject.put("data_status", dataStatus);
        jsonObject.put("status", status);
        JSONObject result = new JSONObject();
        List<SurveyRespondentDto> dtoList = surveyGrpcClient.listRespondent(jsonObject);
        if (ObjectUtil.isEmpty(dtoList)) {
            result.put("count", 0);
            result.put("survey_finished_list", ListUtil.of(0, 0, 0));
        } else {
            // 获取当天日期
            LocalDate now = LocalDate.now();

            // 当天开始时间
            LocalDateTime todayStart = now.atStartOfDay();
            // 当天结束时间
            LocalDateTime todayEnd = LocalDateTime.of(now, LocalTime.MAX);

            // 周一
            LocalDate monday = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
            // 周日
            LocalDate sunday = now.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));

            // 本周开始时间
            LocalDateTime weekStart = monday.atStartOfDay();
            // 本周结束时间
            LocalDateTime weekEnd = LocalDateTime.of(sunday, LocalTime.MAX);

            // 本月1号
            LocalDate firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth());
            // 本月最后一天
            LocalDate lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth());
            LocalDateTime firstDayOfMonthStart = firstDayOfMonth.atStartOfDay();
            LocalDateTime firstDayOfMonthEnd = LocalDateTime.of(lastDayOfMonth, LocalTime.MAX);

            Integer today_rspd_count = Math.toIntExact(dtoList.stream().filter(dto -> dto.getFinishTime().isAfter(todayStart) && dto.getFinishTime().isBefore(todayEnd)).count());
            Integer week_rspd_count = Math.toIntExact(dtoList.stream().filter(dto -> dto.getFinishTime().isAfter(weekStart) && dto.getFinishTime().isBefore(weekEnd)).count());
            Integer month_rspd_count = Math.toIntExact(dtoList.stream().filter(dto -> dto.getFinishTime().isAfter(firstDayOfMonthStart) && dto.getFinishTime().isBefore(firstDayOfMonthEnd)).count());

            result.put("count", dtoList.size());
            result.put("survey_finished_list", ListUtil.of(today_rspd_count, week_rspd_count, month_rspd_count));
        }
        return result;
    }

    @Override
    public Multimap<String, Integer> getFinishedRspdCountV2(String projectId, Integer dataStatus, Integer status) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("project_id", projectId);
        jsonObject.put("data_status", dataStatus);
        jsonObject.put("status", status);
        List<SurveyRespondentDto> dtoList = surveyGrpcClient.listRespondent(jsonObject);

        Multimap<String, Integer> map = ArrayListMultimap.create();
        if (ObjectUtil.isEmpty(dtoList)) {
            map.put("count", 0);
            map.put("survey_finished_list", 0);
            map.put("survey_finished_list", 0);
            map.put("survey_finished_list", 0);
        } else {
            // 获取当天日期
            LocalDate now = LocalDate.now();

            // 当天开始时间
            LocalDateTime todayStart = now.atStartOfDay();
            // 当天结束时间
            LocalDateTime todayEnd = LocalDateTime.of(now, LocalTime.MAX);

            // 周一
            LocalDate monday = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
            // 周日
            LocalDate sunday = now.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));

            // 本周开始时间
            LocalDateTime weekStart = monday.atStartOfDay();
            // 本周结束时间
            LocalDateTime weekEnd = LocalDateTime.of(sunday, LocalTime.MAX);

            // 本月1号
            LocalDate firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth());
            // 本月最后一天
            LocalDate lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth());
            LocalDateTime firstDayOfMonthStart = firstDayOfMonth.atStartOfDay();
            LocalDateTime firstDayOfMonthEnd = LocalDateTime.of(lastDayOfMonth, LocalTime.MAX);

            //DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            //log.info("当天开始时间 = " + todayStart.format(pattern));
            //log.info("当天结束时间 = " + todayEnd.format(pattern));
            //log.info("本周开始时间 = " + weekStart.format(pattern));
            //log.info("本周结束时间 = " + weekEnd.format(pattern));
            //log.info("本月开始时间 = " + firstDayOfMonthStart.format(pattern));
            //log.info("本月结束时间 = " + firstDayOfMonthEnd.format(pattern));

            Integer today_rspd_count = Math.toIntExact(dtoList.stream().filter(dto -> dto.getFinishTime().isAfter(todayStart) && dto.getFinishTime().isBefore(todayEnd)).count());
            Integer week_rspd_count = Math.toIntExact(dtoList.stream().filter(dto -> dto.getFinishTime().isAfter(weekStart) && dto.getFinishTime().isBefore(weekEnd)).count());
            Integer month_rspd_count = Math.toIntExact(dtoList.stream().filter(dto -> dto.getFinishTime().isAfter(firstDayOfMonthStart) && dto.getFinishTime().isBefore(firstDayOfMonthEnd)).count());
            map.put("count", dtoList.size());
            map.put("survey_finished_list", today_rspd_count);
            map.put("survey_finished_list", week_rspd_count);
            map.put("survey_finished_list", month_rspd_count);
        }
        return map;
    }

    /**
     * 转分页：
     * 1、groups除0外全删掉
     * 2、items只保留第一个元素，其他元素移到第一个元素中
     * 转分组：
     * 1、groups保留0，1。1下的data清空，sorts添加所有题目
     * 2、items只保留第一个元素，其他元素移到第一个元素中
     * <p>
     * 另外：items更新pGroupGid
     * """
     *
     * @param structId
     * @param attrJson
     * @param customAttr
     */
    public void resetQstructByOpenGrouping(String structId, JSONObject attrJson, JSONObject customAttr, QdesProject qdesProject) {
        Map<String, Object> map = new HashMap<>();
        map.put("pGroupGid", 0);
        map.put("structId", structId);
        //把前端传的json放进map
        Map<String, Object> attrMap = new HashMap<>(attrJson);
        //前端参数无openGrouping，无需设置
        if (ObjectUtil.isNull(attrMap.get("openGrouping"))) {
            return;
        }
        //把前端传的json放进map
        Map<String, Object> customAttrMap = new HashMap<>(customAttr);
        //原先就已经设置分页模式，样式data无须重新设置
        if (ObjectUtil.isNotNull(customAttrMap.get("openGrouping")) && "0".equals(String.valueOf(customAttrMap.get("openGrouping")))) {
            return;
        }

        QdesQstruct qdesQstruct = qdesQstructMapper.selectById(structId);
        JSONObject data = JSONObject.parseObject(qdesQstruct.getData());
        JSONObject groups = Optional.ofNullable(data)
                .map(it -> it.getJSONObject("groups"))
                .orElse(new JSONObject());
        JSONArray items = Optional.ofNullable(data)
                .map(it -> it.getJSONArray("items"))
                .orElse(new JSONArray());

        JSONArray new_items = new JSONArray();
        JSONObject first_items;
        JSONArray sub_items;
        if (items.size() > 1) {
            first_items = items.getJSONObject(0);
            if (ObjectUtil.isNull(first_items.get("items"))) {
                first_items.put("items", new JSONArray());
            }
            for (int i = 1; i < items.size(); i++) {
                sub_items = Optional.ofNullable(items.getJSONObject(i))
                        .map(it -> it.getJSONArray("items"))
                        .orElse(new JSONArray());
                JSONArray temp_first = first_items.getJSONArray("items");
                temp_first.add(sub_items);
            }
            new_items.add(first_items);
        } else {
            new_items = items;
        }
        JSONObject new_groups = new JSONObject();
        if ("0".equals(String.valueOf(attrMap.get("openGrouping")))) {
            groups.forEach((k, v) -> {
                if ("0".equals(k)) {
                    new_groups.put(k, v);
                }
            });
            qdesQuestionMapper.updateGroupGidByParam(map);
            if (!new_items.isEmpty()) {
                Optional.ofNullable(new_items.getJSONObject(0))
                        .map(it -> it.getJSONArray("items"))
                        .ifPresent(im -> im.forEach(temp -> {
                            JSONObject imObject = (JSONObject) temp;
                            imObject.put("pGroupGid", 0);
                        }));
            }
        }
        Assert.notNull(data, "qstruct data 不能为空");
        data.put("groups", new_groups);
        data.put("items", new_items);
        JSONObject params = new JSONObject();
        params.put("data", data);
        qdesQstructService.updateQstruct(qdesQstruct, params, qdesProject, false, false);
    }

    @Override
    public void templateProject(HttpServletResponse response) throws IOException {
        InputStream in = ResourceUtil.getStream("file/projectTemplate/问卷导入模板.xls");
        response.reset();
        response.setContentType("application/x-msdownload");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("问卷导入模板.xls", "utf-8"));
        ServletOutputStream out = response.getOutputStream();

        IOUtils.copy(in, out);
        IOUtils.closeQuietly(in);
        IOUtils.closeQuietly(out);
    }

    private JSONObject parseProject(JSONObject data, JSONObject attr) {
        log.info("parseProject attr={}", attr);
        log.info("parseProject data={}", data);
        if (attr.getBooleanValue("openGrouping") && data.containsKey("groups")) {
            JSONObject q_dict = new JSONObject();
            for (JSONObject question : walkQstructQuestions(data)) {
                q_dict.put(question.getString("gid"), question);
            }
            JSONObject groups = data.getJSONObject("groups");
            data.remove("groups");

            JSONObject root_grp = fillProjectGroups(groups.getJSONObject("0").getJSONArray("data").getJSONObject(0), groups, q_dict);
            if (root_grp != null && !root_grp.isEmpty()) {
                data.putAll(root_grp);
            }
            data.put("one_question_on_page", true);
            data.put("openGrouping", true);
        }

        data.put("gid", 0);
        log.info("parseProject 222222222222 data={}", data);
        return data;
    }

    private JSONObject fillProjectGroups(JSONObject group, JSONObject group_dict, JSONObject q_dict) {
        String gid = group.getString("gid");
        JSONObject pgroup = group_dict.getJSONObject(gid);
        if (pgroup.isEmpty()) {
            return null;
        }
        JSONObject data;
        Object data_tmp = pgroup.get("data");
        if (data_tmp instanceof List) {
            JSONArray array = (JSONArray) data_tmp;
            data = new JSONObject();
            for (int i = 0; i < array.size(); i++) {
                JSONObject d = array.getJSONObject(i);
                data.put(d.getString("gid"), d);
            }
        } else {
            data = (JSONObject) data_tmp;
        }

        JSONArray items = new JSONArray();
        JSONArray sorts = pgroup.getJSONArray("sorts");
        for (int i = 0; i < sorts.size(); i++) {
            JSONObject d = sorts.getJSONObject(i);
            String type = d.getString("type");
            String forgid = d.getString("gid");
            if (StrUtil.isNotBlank(forgid) && Integer.parseInt(forgid) > 0) {
                if ("G".equals(type)) {
                    JSONObject sub_grp = data.getJSONObject(gid);
                    if (sub_grp.isEmpty()) {
                        continue;
                    }
                    sub_grp = fillProjectGroups(sub_grp, group_dict, q_dict);
                    if (sub_grp != null && !sub_grp.isEmpty()) {
                        items.add(sub_grp);
                    }
                } else if ("Q".equals(type)) {
                    JSONObject q = q_dict.getJSONObject(gid);
                    if (!q.isEmpty()) {
                        items.add(q);
                    }
                }
            }
        }
        if (!items.isEmpty()) {
            group.put("items", items);
        }
        return group;
    }

    @EqualsAndHashCode(callSuper = true)
    @Data
    public static class QdesProjectDto extends BaseEntity {

        @JsonProperty("id")
        private String id;

        @JsonProperty("code")
        private String code;

        @JsonProperty("title")
        private String title;

        @JsonProperty("creator")
        private String creator;

        @JSONField(name = "companyID")
        @JsonProperty("companyId")
        private String companyId;

        @JSONField(serializeUsing = StringValue2FastJsonSerializer.class)
        @JsonProperty("groupList")
        private String groupList;

        @JsonProperty("groupListList")
        private List<String> groupListList;

        @JsonProperty("version")
        private Integer version;

        @JsonProperty("releaseVer")
        private Integer releaseVer;

        @JsonProperty("status")
        private Integer status;

        @JSONField(name = "structID")
        @JsonProperty("structId")
        private String structId;

        @JSONField(name = "logicID")
        @JsonProperty("logicId")
        private String logicId;

        @JSONField(name = "customAttr", serializeUsing = StringValue2FastJsonSerializer.class)
        @JsonProperty("customAttr")
        private String customAttr;

        @JsonProperty("source")
        private String source;

        @JsonProperty("extId")
        @JSONField(name = "extID")
        private String extId;

        @JsonProperty("timerExpire")
        @JSONField(name = "timer_expire")
        private Integer timerExpire;

        @JsonProperty("ptype")
        private Integer ptype;

        @JSONField(name = "is_copy_demo")
        @JsonProperty("isCopyDemo")
        private Boolean isCopyDemo;

        @JSONField(name = "worktable_info", serializeUsing = StringValue2FastJsonSerializer.class)
        @JsonProperty("worktableInfo")
        private String worktableInfo;

        @JSONField(serializeUsing = StringValue2FastJsonSerializer.class)
        @JsonProperty("qstruct")
        private String qstruct;
    }

    private XmResultJson<QdesImportProjectVo> importProjectFromExcel(MultipartFile file, ProjectImportParam param,
                                                                     QdesProject project) {
        QdesImportProjectVo projectVo = new QdesImportProjectVo();

        // 根据excel导入
        try (InputStream inputStream = file.getInputStream()) {

            JSONObject excelData = parseExcelData(inputStream);
            JSONArray questionList = excelData.getJSONArray("question_list");
            Integer totalGidCount = excelData.getInteger("total_gid_count");
            // 校验题目cid, 选项cid, 组cid重复
            if (questionList == null || questionList.isEmpty()) {
                throw new BusinessException("缺少问卷内容sheet");
            }

            List<String> errorMsgs = checkExcelValid(questionList);
            if (CollectionUtils.isNotEmpty(errorMsgs)) {
                return XmResultJson.fail(400, String.join(",", errorMsgs));
            }
            // 设置导入文件名称为项目名称
            String fileName = file.getOriginalFilename();
            String name = StringUtils.isEmpty(fileName) ? "file" : fileName.substring(0, fileName.lastIndexOf("."));
            project.setTitle(name);
            QdesProject oneProject = createOneProject(project, true);
            projectExcelImport0(oneProject, totalGidCount, questionList);
            projectVo.setPid(oneProject.getId());
            projectVo.setSid(oneProject.getStructId());
        } catch (IOException e) {
            log.error("读取文件失败", e);
            throw new BusinessException("读取文件异常");
        }

        return XmResultJson.success(projectVo);
    }

    private void projectExcelImport0(QdesProject project, Integer totalGidCount, JSONArray questionList) {
        // 问卷Excel导入
        String projectId = project.getId();
        String projectTitle = project.getTitle();
        String structId = project.getStructId();
        JSONObject re = excelQuestionsPackage(projectId, totalGidCount, questionList);
        JSONArray preparedQuestions = re.getJSONArray("prepared_questions");
        JSONObject relatedRefDict = re.getJSONObject("relatedRefDict");
        JSONObject qcidQgidDict = re.getJSONObject("qcid_qgid_dict");
        JSONArray gids = re.getJSONArray("gids");
        int gidIndex = re.getInteger("gid_index");

        JSONArray pages = new JSONArray();
        JSONObject appendGroups = new JSONObject();

        // 添加题目分组
        String customAttr = project.getCustomAttr();
        int openGrouping = 0;
        if (StringUtils.isNotEmpty(customAttr)) {
            openGrouping = Optional.ofNullable(JSONObject.parseObject(customAttr).getInteger("openGrouping")).orElse(0); // 0:分页 1：分组
        }
        if (openGrouping != 0) {
            // 分组模式
            // 分页模式 导出的EXCEL，group sheet 是分页结构，需要补上根分组：题目
            boolean isGroupSheet = false;
            if (!isGroupSheet) {
                // 分组模式处理分页组
                // groupList一直为空，因为不涉及分组
                // 处理组sheet未设置分组的情况
                int pgid = 1;
                JSONObject pageJson = getPageSampleJson();
                pageJson.put("cid", "P1");
                pageJson.put("gid", pgid);
                pageJson.put("_index", pgid);
                JSONArray items = new JSONArray();
                JSONArray sortJs = new JSONArray();
                for (int i = 0; i < questionList.size(); i++) {
                    JSONObject q = questionList.getJSONObject(i);
                    JSONObject qJ = new JSONObject();
                    qJ.put("_index", q.get("_index"));
                    qJ.put("pGroupGid", 1);
                    qJ.put("disp_code", q.get("disp_code"));
                    qJ.put("gid", q.get("gid"));
                    items.add(qJ);
                    JSONObject sortJ = new JSONObject();
                    sortJ.put("type", "Q");
                    sortJ.put("gid", q.get("gid"));
                    sortJs.add(sortJ);
                }
                pageJson.put("items", items);
                JSONObject oneJs = new JSONObject();
                oneJs.put("data", new JSONArray());
                oneJs.put("sorts", sortJs);
                appendGroups.put("1", oneJs);
                pages.add(pageJson);
            } else {
                gidIndex += 1;
                Object pgid = gids.get(gidIndex);
                JSONObject pageJson = getPageSampleJson();
                pageJson.put("cid", "P1");
                pageJson.put("gid", pgid);
                pageJson.put("_index", pgid);
                pageJson.put("items", new JSONArray());
                pages.add(pageJson);
            }
        } else {
            // 分页模式
            //        # 分组模式导出的EXCEL，group sheet 是分组结构，丢弃G:信息
            //        # S1,Q1,G:G2,Q2 -> S1,Q2
            // group_list为空时，补默认根页
            JSONObject pageJson = getPageSampleJson();
            int pgid = 1;
            pageJson.put("cid", "P1");
            pageJson.put("gid", pgid);
            pageJson.put("_index", pgid);
            JSONArray items = new JSONArray();
            for (int i = 0; i < questionList.size(); i++) {
                JSONObject q = questionList.getJSONObject(i);
                JSONObject qJ = new JSONObject();
                qJ.put("_index", q.get("_index"));
                qJ.put("gid", q.get("gid"));
                items.add(qJ);
            }
            pageJson.put("items", items);
            pages.add(pageJson);
        }

        JSONObject structData = getStructDataSample();
        String beginDesc1 = structData.getJSONObject("desc_begin_page").getJSONObject("desc").getString("text");
        String endDesc1 = structData.getJSONObject("desc_end_page").getJSONObject("content").getString("text");
        String screenoutDesc1 = structData.getJSONObject("desc_end_page").getJSONObject("content").getString("screen_out");
        String quotaFull1 = structData.getJSONObject("desc_end_page").getJSONObject("content").getString("quota_full");

        beginDesc1 = beginDesc1.replace("'", "\"");
        endDesc1 = endDesc1.replace("'", "\"");
        screenoutDesc1 = screenoutDesc1.replace("'", "\"");
        quotaFull1 = quotaFull1.replace("'", "\"");

        structData.getJSONObject("desc_begin_page").getJSONObject("desc").put("text", beginDesc1);
        structData.getJSONObject("desc_end_page").getJSONObject("content").put("text", endDesc1);
        structData.getJSONObject("desc_end_page").getJSONObject("content").put("screen_out", screenoutDesc1);
        structData.getJSONObject("desc_end_page").getJSONObject("content").put("quota_full", quotaFull1);

        structData.getJSONObject("desc_begin_page").getJSONObject("content").put("text", projectTitle);

        structData.put("items", pages);
        JSONObject structGroups = structData.getJSONObject("groups");
        if (!appendGroups.isEmpty()) {
            structGroups.putAll(appendGroups);
        }
        structData.put("selectedTheme", "default");
        if (relatedRefDict != null && !relatedRefDict.isEmpty()) {
            structData.put("relatedRefs", relatedRefDict);
        }

        //  更新struct
        JSONObject param = new JSONObject();
        param.put("data", structData);
        QdesQstruct qstruct = qdesQstructMapper.selectById(structId);
        qdesQstructService.updateQstruct(qstruct, param, project, false, true);
        // 创建问卷
        List<QdesQuestion> questions = new ArrayList<>();
        for (int j = 0; j < preparedQuestions.size(); j++) {
            JSONObject params = preparedQuestions.getJSONObject(j);
            QdesQuestion question = new QdesQuestion();
            question.setStructId(structId);
            question.setGid(params.getInteger("gid"));
            question.setData(params.toJSONString());
            questions.add(question);
        }
        if (CollectionUtils.isNotEmpty(questions)) {
            qdesQuestionMapper.insertBatchSomeColumn(questions);
        }
    }

    private JSONObject getPageSampleJson() {
        String attr = ResourceUtil.readUtf8Str(PAGE_FILE_PATH);
        return JSON.parseObject(attr);
    }


    private JSONObject excelQuestionsPackage(String projectId, Integer totalGidCount, JSONArray questionList) {
        // Excel导入的题目补gid，生成关联信息
        List<Integer> gids = acquireProjectGid(projectId, totalGidCount);
        List<JSONObject> preparedQuestions = new ArrayList<>();
        int gidIndex = 1; // gid 从2开始
        JSONObject qcidQgidDict = new JSONObject();
        for (int i = 0; i < questionList.size(); i++) {
            JSONObject qDict = questionList.getJSONObject(i);
            int qIndex = i + 1;
            gidIndex += 1;
            Integer qgid = gids.get(gidIndex);
            String qcid = qDict.getString("cid");
            qcidQgidDict.put(qcid, qDict);

            qDict.put("gid", qgid);
            qDict.put("_index", qIndex);
            qDict.put("seq", qIndex); // 题目 补gid

            JSONArray items = Optional.ofNullable(qDict.getJSONArray("items")).orElse(new JSONArray()); // 选项
            JSONArray optionGroupList = Optional.ofNullable(qDict.getJSONArray("option_group_list")).orElse(new JSONArray()); // 选项分组
            JSONArray optionBindList = Optional.ofNullable(qDict.getJSONArray("option_bind_list")).orElse(new JSONArray()); // 选项绑定
            JSONArray optionExcludeList = Optional.ofNullable(qDict.getJSONArray("option_exclude_list")).orElse(new JSONArray()); // 选项
            JSONArray rowsItems = Optional.ofNullable(qDict.getJSONArray("rows_items")).orElse(new JSONArray()); // 矩阵行
            JSONArray rowGroupList = Optional.ofNullable(qDict.getJSONArray("row_group_list")).orElse(new JSONArray()); // 矩阵行分组
            JSONArray rowBindList = Optional.ofNullable(qDict.getJSONArray("row_bind_list")).orElse(new JSONArray()); // 矩阵行绑定

            // 选项 补gid
            JSONObject ogidDict = new JSONObject();

            for (int j = 0; j < items.size(); j++) {
                JSONObject itemDict = items.getJSONObject(j);
                gidIndex += 1;
                Integer ogid = gids.get(gidIndex);
                itemDict.put("gid", ogid);
                itemDict.put("seq", ogid);
                ogidDict.put(itemDict.getString("oid"), itemDict.get("gid")); // 选项oid: ogid 字典
            }

            JSONObject ogroupDict = new JSONObject();  // 选项oid: 分组id 字典
            // 选项组 补id
            for (int j = 0; j < optionGroupList.size(); j++) {
                int ogIndex = j + 1;
                JSONObject itemDict = optionGroupList.getJSONObject(j);
                String ogroupId = qgid + "_" + ogIndex;
                for (JSONObject oitemDict : Optional.ofNullable(itemDict.getJSONArray("items"))
                        .orElse(new JSONArray()).toJavaList(JSONObject.class)) {
                    Object tmpOgid = ogidDict.get(oitemDict.getString("oid"));
                    oitemDict.put("gid", tmpOgid);
                    ogroupDict.put(oitemDict.getString("oid"), ogroupId);
                }
                // 其他分组 id 命名:qgid_OTHER_GROUP
                if (ogIndex == optionGroupList.size() && "其他分组".equals(itemDict.getString("title"))) {
                    ogroupId = qgid + "_OTHER_GROUP";
                }
                itemDict.put("id", ogroupId);
            }

            // 选项绑定 补id
            for (int j = 0; j < optionBindList.size(); j++) {
                int obIndex = j + 1;
                JSONObject bindDict = optionGroupList.getJSONObject(j);
                String obindId = qgid + "_" + obIndex;
                for (JSONObject tmpItemDict : Optional.ofNullable(bindDict.getJSONArray("items"))
                        .orElse(new JSONArray()).toJavaList(JSONObject.class)) {
                    String tmpOid = tmpItemDict.getString("oid");
                    Object tmpOgid = ogidDict.get(tmpOid);
                    Object pGroupId = ogroupDict.get(tmpOid);

                    tmpItemDict.put("gid", tmpOgid); // 补选项 gid
                    if (pGroupId != null) {
                        tmpItemDict.put("pGroupId", pGroupId); // 选项分组，补pGroupId
                    }
                }
                bindDict.put("id", obindId); // 补选项绑定 组id
            }

            // 选项排他 补id
            for (int j = 0; j < optionExcludeList.size(); j++) {
                int oeIndex = j + 1;
                JSONObject optionExcludeDict = optionExcludeList.getJSONObject(j);
                String oexcludeId = qgid + "_" + oeIndex;
                for (JSONObject tmpItemDict : Optional.ofNullable(optionExcludeDict.getJSONArray("items"))
                        .orElse(new JSONArray()).toJavaList(JSONObject.class)) {
                    String tmpOid = tmpItemDict.getString("oid");
                    Object tmpRgid = ogidDict.get(tmpOid);

                    tmpItemDict.put("gid", tmpRgid); // 补选项 gid
                }
                optionExcludeDict.put("id", oexcludeId); // 补选项排他 组id
            }
            // 矩阵行 补id
            JSONObject rgidDict = new JSONObject();
            for (int j = 0; j < rowsItems.size(); j++) {
                gidIndex += 1;
                JSONObject itemDict = rowsItems.getJSONObject(j);
                itemDict.put("gid", gids.get(gidIndex));
                itemDict.put("seq", gids.get(gidIndex));
                rgidDict.put(itemDict.getString("oid"), itemDict.get("gid")); // 矩阵行oid: ogid 字典
            }

            JSONObject rgroupDict = new JSONObject(); // 矩阵行oid: 分组id 字典
            // 矩阵行分组 补id
            for (int j = 0; j < rowGroupList.size(); j++) {
                int rgIndex = j + 1;
                JSONObject rgItemDict = rowGroupList.getJSONObject(j);
                String rgroupId = qgid + "_" + rgIndex;
                for (JSONObject ritemDict : Optional.ofNullable(rgItemDict.getJSONArray("items"))
                        .orElse(new JSONArray()).toJavaList(JSONObject.class)) {
                    Object tmpRgid = rgidDict.get(ritemDict.getString("oid"));
                    ritemDict.put("gid", tmpRgid);
                    ogroupDict.put(ritemDict.getString("oid"), rgroupId);
                }
                // 其他分组 id 命名:qgid_OTHER_GROUP
                if (rgIndex == rowGroupList.size() && "其他分组".equals(rgItemDict.getString("title"))) {
                    rgroupId = qgid + "_OTHER_GROUP";
                }
                rgItemDict.put("id", rgroupId);
            }

            // 矩阵行绑定 补id
            for (int j = 0; j < rowBindList.size(); j++) {
                int rbIndex = j + 1;
                JSONObject rowBindDict = rowBindList.getJSONObject(j);
                String rbindId = qgid + "_" + rbIndex;
                for (JSONObject tmpItemDict : Optional.ofNullable(rowBindDict.getJSONArray("items"))
                        .orElse(new JSONArray()).toJavaList(JSONObject.class)) {
                    String tmpRoid = tmpItemDict.getString("oid");
                    Object tmpRgid = rgidDict.get(tmpRoid);
                    Object pGroupId = rgroupDict.get(tmpRoid);

                    tmpItemDict.put("gid", tmpRgid); // 补选项 gid
                    if (pGroupId != null) {
                        tmpItemDict.put("pGroupId", pGroupId); // 选项分组，补pGroupId
                    }
                }
                rowBindDict.put("id", rbindId); // 补选项绑定 组id
            }
        }

        JSONObject relatedRefDict = new JSONObject();
        for (int i = 0; i < questionList.size(); i++) {
            JSONObject qDict = questionList.getJSONObject(i);
            Object qgid = qDict.get("gid");
            JSONArray relatedRefs = Optional.ofNullable(qDict.getJSONArray("relatedRefs")).orElse(new JSONArray());
            qDict.remove("relatedRefs");
            if (!relatedRefs.isEmpty()) {
                // 处理选项关联
                for (int j = 0; j < relatedRefs.size(); j++) {
                    JSONObject relatedDict = relatedRefs.getJSONObject(j);
                    String sourceQcid = Optional.ofNullable(relatedDict.getString("sourceQcid")).orElse("");
                    JSONObject sourceq = qcidQgidDict.getJSONObject(sourceQcid);
                    relatedDict.put("sourceGid", sourceq.get("gid"));
                    relatedDict.put("targetGid", qgid);
                }
                relatedRefDict.put(qgid.toString(), relatedRefs);
            }
            preparedQuestions.add(qDict);
        }

        JSONObject re = new JSONObject();
        re.put("prepared_questions", preparedQuestions);
        re.put("relatedRefDict", relatedRefDict);
        re.put("qcid_qgid_dict", qcidQgidDict);
        re.put("gids", gids);
        re.put("gid_index", gidIndex);
        return re;
    }

    @Override
    public List<Integer> acquireProjectGid(String projectId, Integer num) {
        // 获取问卷gid，支持批量获取
        Integer maxGid = serviceSequenceService.getSequence("proj_gid_" + projectId, num);
        if (maxGid == null) {
            QdesProject project = qdesProjectMapper.selectById(projectId);
            maxGid = 0;
            if (project != null) {
                QdesQstruct qdesQstruct = qdesQstructMapper.selectById(project.getStructId());
                if (qdesQstruct != null) {
                    JSONObject datajs = StringUtils.isEmpty(qdesQstruct.getData()) ? new JSONObject()
                            : JSONObject.parseObject(qdesQstruct.getData());
                    maxGid = datajs.getInteger("max_gid") == null ? 0 : datajs.getInteger("max_gid");
                }
            }
            maxGid += num;
            maxGid = serviceSequenceService.getSequence("proj_gid_" + projectId, maxGid);
        }
        List<Integer> allGids = new ArrayList<>();
        for (int i = num - 1; i >= 0; i--) {
            allGids.add(maxGid - i);
        }
        return allGids;
    }

    private JSONObject parseExcelData(InputStream inputStream) {
        JSONObject reJs = new JSONObject();
        try {
            log.info("Now to parseExcelData");
            XSSFWorkbook xssfWorkbook = new XSSFWorkbook(inputStream);
            XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
            int maxRow = sheet.getLastRowNum();
            List<List<String>> dataList = new ArrayList<>();
            List<String> groupList = new ArrayList<>();
            for (int i = 1; i <= maxRow; i++) {
                List<String> rowList = new ArrayList<>();
                XSSFRow row = sheet.getRow(i);
                if (row == null) {
                    continue;
                }
                for (int j = 0; j <= row.getLastCellNum(); j++) {
                    String cell = null == row.getCell(j) ? "" : row.getCell(j).toString();
                    rowList.add(cell.replace(".0", "").trim());
                }
                if (CollectionUtils.isNotEmpty(rowList)) {
                    dataList.add(rowList);
                }
            }
            if (log.isInfoEnabled()) {
                log.info("Now the dataList = {}", dataList);
            }
            // 处理data sheet
            int totalGidCount = 20;  // 申请gid预留20个备用
            List<Integer> questionIndexList = new ArrayList<>();
            for (int i = 0; i < dataList.size(); i++) {
                List<String> dataRow = dataList.get(i);
                if (dataRow.size() < 3) {
                    continue;
                }
                String qcidQtypeStr = dataRow.get(0);
                String qcid = "";
                String qtypeDesc = "";
                if (StringUtils.isNotEmpty(qcidQtypeStr)) {
                    if (!qcidQtypeStr.contains("[") || !qcidQtypeStr.contains("]")) {
                        continue;
                    }
                    qcid = qcidQtypeStr.split("\\[")[0];
                    qtypeDesc = qcidQtypeStr.split("\\[")[1].split("]")[0];
                    // Q17[多项填空题] ==> ['Q17', '多项填空题']
                }

                if (StringUtils.isNotEmpty(qcid) && StringUtils.isNotEmpty(qtypeDesc)
                        && Arrays.stream(QuestionDescTypeEnum.values()).map(QuestionDescTypeEnum::getDesc)
                        .collect(Collectors.toList()).contains(qtypeDesc)) {
                    questionIndexList.add(i);
                }
            }
            totalGidCount += questionIndexList.size();
            JSONArray questionList = new JSONArray();
            for (int i = 0; i < questionIndexList.size(); i++) {
                int j = questionIndexList.get(i);
                List<String> questionLine = dataList.get(j);
                JSONObject qDict = new JSONObject();
                String qcidQtypeStr = questionLine.get(0);
                if (!qcidQtypeStr.contains("[") || !qcidQtypeStr.contains("]")) {
                    continue;
                }
                String qcid = qcidQtypeStr.split("\\[")[0];
                String qtypeDesc = qcidQtypeStr.split("\\[")[1].split("]")[0];
                Optional<QuestionDescTypeEnum> descType = QuestionDescTypeEnum.getOneQuestionDescType(qtypeDesc);
                if (!descType.isPresent()) {
                    continue;
                }
                String dispType = descType.get().getDispType();
                String customQtype = descType.get().getCustomQtype();
                if (Arrays.asList(QUESTION_TYPE_CASCADE, QUESTION_TYPE_EVALUATION).contains(dispType)
                        || (QUESTION_TYPE_SINGLE.equals(dispType) && "image_select".equals(customQtype))
                        || (QUESTION_TYPE_MULTIPLE.equals(dispType) && "image_select".equals(customQtype))) {
                    continue;
                }
                // 解析题目属性
                qDict.putAll(parseQuestionAttr(questionLine, qcid, dispType, customQtype));

                List<List<String>> questionContentList;
                if (i != questionIndexList.size() - 1) {
                    questionContentList = dataList.subList(j + 1, questionIndexList.get(i + 1));
                } else {
                    questionContentList = dataList.subList(j + 1, dataList.size());
                }

                // 原始标记列的字典，值为list
                JSONObject rawGroupDict = new JSONObject();
                JSONArray tmpGroup = new JSONArray();
                for (List<String> content : questionContentList) {
                    String A0 = content.get(0);
                    String A1 = content.get(1);
                    if (StringUtils.isEmpty(A0)) {
                        continue;
                    }
                    boolean isMatch = KEYWORDS.contains(A0) && StringUtils.isEmpty(A1);
                    if (isMatch) {
                        tmpGroup = rawGroupDict.getJSONArray(A0);
                        if (tmpGroup == null) {
                            tmpGroup = new JSONArray();
                        }
                        tmpGroup.add(content);
                        rawGroupDict.put(A0, tmpGroup);
                        continue;
                    }
                    if (!tmpGroup.isEmpty()) {
                        tmpGroup.add(content);
                    }
                }

                for (String key : rawGroupDict.keySet()) {
                    JSONArray rawItems = rawGroupDict.getJSONArray(key);
                    KeyWordHandler typeHandler = keyWordHandlers.stream()
                            .filter(it -> Arrays.stream(it.getType().split(";")).collect(Collectors.toList()).contains(key))
                            .findFirst().orElse(null);
                    if (typeHandler == null) {
                        log.error("Error to get typeHandler, key = {}", key);
                        continue;
                    }
                    Integer totalGidCountTmp = typeHandler.handler(dispType, rawItems, qDict);
                    if (totalGidCountTmp != null) {
                        totalGidCount += totalGidCountTmp;
                    }
                }

                // 判断q_dict是否有items属性
                if (qDict.getJSONArray("items") == null) {
                    qDict.put("items", new JSONArray());
                }
                questionList.add(qDict);
            }
            reJs.put("question_list", questionList);
            reJs.put("group_list", groupList); // groupList 一直是空
            reJs.put("total_gid_count", totalGidCount);
        } catch (IOException e) {
            log.error("[QdesProject]Analysis excel error, e = ", e);
            throw new BusinessException(e.getMessage());
        }
        return reJs;
    }

    private JSONObject parseQuestionAttr(List<String> questionLine, String qcid, String dispType, String customQtype) {
        // 解析题目属性
        JSONObject questionDict = new JSONObject();
        String qtitle = questionLine.get(1);
        String qAttrStr = questionLine.get(2);
        String dispPlasCode = questionLine.size() > 3 ? questionLine.get(3) : "";
        String jumpCode = questionLine.size() > 4 ? questionLine.get(4) : "";
        String autoCode = questionLine.size() > 5 ? questionLine.get(5) : "";
        questionDict.put("qtype", dispType);
        questionDict.put("custom_qtype", customQtype);
        questionDict.put("title", qtitle);
        questionDict.put("cid", qcid);
        questionDict.put("type", "Q");

        boolean isBlank = QUESTION_TYPE_BLANK.getValue().equals(dispType);
        boolean isScore = QUESTION_TYPE_MATRIX_SCORE.getValue().equals(dispType)
                || QUESTION_TYPE_SCORE.getValue().equals(dispType)
                || QUESTION_CUSTOM_TYPE_SCORE_CURSOR_SCORE.getCustomQtype().equals(customQtype);
        boolean isCity = QUESTION_TYPE_CITY.getValue().equals(dispType);
        boolean isAutotable = QUESTION_TYPE_AUTOTABLE.getDispType().equals(dispType);

        questionDict.putAll(defaultQuestionAttr(dispType, customQtype, dispPlasCode, questionDict));

        if (StringUtils.isNotEmpty(qAttrStr)) {
            List<String> attributes = new ArrayList<>();
            Pattern attrPattern = Pattern.compile("\\[(.*?)\\]");
            Matcher attrMatcher = attrPattern.matcher(qAttrStr);
            while (attrMatcher.find()) {
                attributes.add(attrMatcher.group(1));
            }
            for (String attr : attributes) {
                if ("非必答".equals(attr)) {
                    questionDict.put("required", false);
                } else if ("必答".equals(attr)) {
                    questionDict.put("required", true);
                } else if ("投票统计".equals(attr)) {
                    questionDict.put("vote_setting", true);
                } else if ("下拉单选".equals(attr)) {
                    questionDict.put("answerStyle", "select");
                } else if ("显示代码".equals(attr)) {
                    questionDict.put("dispSelect", 1);
                    questionDict.put("codeSelect", true);
                    questionDict.put("disp_code", dispPlasCode);
                } else if ("抹灰代码".equals(attr)) {
                    questionDict.put("dispSelect", 1);
                    questionDict.put("codeSelect", false);
                    questionDict.put("plaster_code", dispPlasCode);
                } else if ("行列互换".equals(attr)) {
                    questionDict.put("swap", true);
                } else if (attr.startsWith("最少选")) {
                    Pattern pattern = Pattern.compile("最少选(\\d+)项");
                    Matcher matcher = pattern.matcher(attr);
                    if (matcher.find()) {
                        String minNumb = matcher.group(1);
                        JSONArray optionsRange = questionDict.getJSONArray("options_range");
                        if (optionsRange == null) {
                            optionsRange = new JSONArray();
                            optionsRange.add(0);
                            optionsRange.add(0);
                        }
                        optionsRange.set(0, Integer.parseInt(minNumb));
                        questionDict.put("options_range", optionsRange);
                    }
                } else if (attr.startsWith("最多选")) {
                    Pattern pattern = Pattern.compile("最多选(\\d+)项");
                    Matcher matcher = pattern.matcher(attr);
                    if (matcher.find()) {
                        String maxNumb = matcher.group(1);
                        JSONArray optionsRange = questionDict.getJSONArray("options_range");
                        if (optionsRange == null) {
                            optionsRange = new JSONArray();
                            optionsRange.add(0);
                            optionsRange.add(0);
                        }
                        optionsRange.set(1, Integer.parseInt(maxNumb));
                        questionDict.put("options_range", optionsRange);
                    }
                } else if ("不允许手动修正位置".equals(attr)) {
                    questionDict.put("modifyChangeLocation", true);
                } else if (Pattern.compile("比重总和#(\\d+)").matcher(attr).find()) {
                    questionDict.put("total", Integer.parseInt(attr.split("#")[1]));
                } else if (attr.startsWith("输入框高度")) {
                    String val = attr.split("#")[1];
                    questionDict.put("inputbox_height", "小".equals(val) ? "small" : "中".equals(val) ? "middle" : "大"
                            .equals(val) ? "big" : "");
                } else {
                    if (isBlank) {
                        if (Pattern.compile("^(\\d+-\\d+-\\d+|\\d+-\\d+)$").matcher(attr).find()) {
                            questionDict.put("words_number_range", Arrays.stream(attr.split("-")).map(Integer::parseInt).collect(Collectors.toList()));
                        } else if ("唯一".equals(attr)) {
                            questionDict.put("submit_once", true);
                        } else if ("短信验证".equals(attr)) {
                            questionDict.put("open_validate", true);
                        } else if (Pattern.compile("^(年-月-日|年-月|年)$").matcher(attr).find()) {
                            String rangeTxt = "";
                            switch (attr) {
                                case "年":
                                    rangeTxt = "yyyy";
                                    break;
                                case "年-月":
                                    rangeTxt = "yyyy-MM";
                                    break;
                                case "年-月-日":
                                    rangeTxt = "yyyy-MM-dd";
                                    break;
                                default:
                                    rangeTxt = "";
                            }
                            questionDict.put("words_number_range", rangeTxt);
                        } else {
                            Object contentType = deOpenContentLimitTrans(attr);
                            if (contentType != null) {
                                questionDict.put("content_type", contentType);
                            }
                        }
                    } else if (isScore) {
                        if (attr.startsWith("图标#")) {
                            questionDict.put("icon", attr.split("#")[1]);
                        } else if (!QUESTION_CUSTOM_TYPE_SCORE_NPS_SCORE.getCustomQtype().equals(customQtype) && attr.startsWith("最低")) {
                            Pattern pattern = Pattern.compile("最低(\\d+)分");
                            Matcher matcher = pattern.matcher(attr);
                            if (matcher.find()) {
                                String minNumb = matcher.group(1);
                                JSONArray scoreRange = questionDict.getJSONArray("score_range");
                                if (scoreRange == null) {
                                    scoreRange = new JSONArray();
                                    scoreRange.add(0);
                                    scoreRange.add(0);
                                }
                                scoreRange.set(0, Integer.parseInt(minNumb));
                                questionDict.put("score_range", scoreRange);
                            }
                        } else if (!QUESTION_CUSTOM_TYPE_SCORE_NPS_SCORE.getCustomQtype().equals(customQtype) && attr.startsWith("最多")) {
                            Pattern pattern = Pattern.compile("最多(\\d+)分");
                            Matcher matcher = pattern.matcher(attr);
                            if (matcher.find()) {
                                String maxNumb = matcher.group(1);
                                JSONArray scoreRange = questionDict.getJSONArray("score_range");
                                if (scoreRange == null) {
                                    scoreRange = new JSONArray();
                                    scoreRange.add(0);
                                    scoreRange.add(0);
                                }
                                scoreRange.set(1, Integer.parseInt(maxNumb));
                                questionDict.put("score_range", scoreRange);
                            }
                        } else if (attr.startsWith("提示文字#")) {
                            String val = attr.split("#")[1];
                            JSONObject promptText = questionDict.getJSONObject("prompt_text");
                            if (promptText == null) {
                                promptText = new JSONObject();
                            }
                            String[] txts = val.split(",");
                            if (txts.length == 3) {
                                promptText.put("left_words", txts[0]);
                                promptText.put("middle_words", txts[1]);
                                promptText.put("right_words", txts[2]);
                                questionDict.put("prompt_text", promptText);
                            }
                        }
                    } else if (isCity) {
                        if (attr.startsWith("地址#")) {
                            String val = attr.split("#")[1];
                            Integer infoLevel = null;
                            switch (val) {
                                case "省份":
                                    infoLevel = 1;
                                    break;
                                case "城市":
                                    infoLevel = 2;
                                    break;
                                case "区县":
                                    infoLevel = 3;
                                    break;
                                case "街道":
                                    infoLevel = 4;
                                    break;
                                default:
                            }
                            questionDict.put("info_level", infoLevel == null ? "" : infoLevel);
                        }
                    } else if (isAutotable) {
                        if (Pattern.compile("默认行数#(\\d+)").matcher(attr).find()) {
                            questionDict.put("defaultRows", Integer.parseInt(attr.split("#")[1].trim()));
                        } else if (Pattern.compile("最大行数#(\\d+)").matcher(attr).find()) {
                            questionDict.put("maxRows", Integer.parseInt(attr.split("#")[1].trim()));
                        }
                    }
                }
            }
        }

        if (StringUtils.isNotEmpty(jumpCode)) {
            questionDict.put("jumpSelect", 1);
            questionDict.put("jump_code", jumpCode);
        }
        if (StringUtils.isNotEmpty(autoCode)) {
            questionDict.put("auto_code", autoCode);
        }

        return questionDict;
    }

    private Object deOpenContentLimitTrans(String openType) {
        // 开放框内容限制 文本反推
        if ("不限".equals(openType)) {
            return 0;
        } else if ("整数".equals(openType)) {
            return "int";
        } else if ("小数".equals(openType)) {
            return "number";
        } else if ("字母".equals(openType)) {
            return "lang_en";
        } else if ("中文".equals(openType)) {
            return "lang_ch";
        } else if ("邮件".equals(openType)) {
            return "email";
        } else if ("日期".equals(openType)) {
            return "date";
        } else if ("手机号码".equals(openType)) {
            return "mobile";
        } else if ("固话".equals(openType)) {
            return "tel";
        } else if ("手机/固话".equals(openType)) {
            return "mobile_tel";
        } else if ("身份证号".equals(openType)) {
            return "idCard";
        }
        return null;
    }

    private JSONObject defaultQuestionAttr(String dispType, String customQtype, String dispPlasCode, JSONObject questionDict) {
        // 问卷题目默认属性
        if (questionDict == null || questionDict.isEmpty()) {
            questionDict = new JSONObject();
        }
        boolean isBlank = QUESTION_TYPE_BLANK.getValue().equals(dispType);
        boolean isScore = QUESTION_TYPE_MATRIX_SCORE.getValue().equals(dispType)
                || QUESTION_TYPE_SCORE.getValue().equals(dispType)
                || QUESTION_CUSTOM_TYPE_SCORE_CURSOR_SCORE.getCustomQtype().equals(dispType);
        boolean isCity = QUESTION_TYPE_CITY.getValue().equals(dispType);
        boolean isAutotable = QUESTION_TYPE_AUTOTABLE.getDispType().equals(dispType);

        boolean isMatrixScore = QUESTION_TYPE_MATRIX_SCORE.getValue().equals(dispType);
        boolean isProportion = QUESTION_TYPE_PROPORTION.getValue().equals(dispType); // 比重题
        boolean isChoiceQuestion = QUESTION_TYPE_SINGLE.equals(dispType) || QUESTION_TYPE_MULTIPLE.equals(dispType);
        boolean isMatrix = Arrays.asList(QUESTION_TYPE_MATRIX_SINGLE.getValue(), QUESTION_TYPE_MATRIX_MULTIPLE.getValue(),
                QUESTION_TYPE_MATRIX_BLANK.getValue(), QUESTION_TYPE_MATRIX_SCORE.getValue()).contains(dispType);
        boolean isMutipileBlank = QUESTION_TYPE_MULTIPLE_BLANK.getValue().equals(dispType);

        questionDict.put("required", true);
        // 设置默认属性
        // 如果显示代码/抹灰代码列，默认是显示代码
        if (StringUtils.isNotEmpty(dispPlasCode)) {
            questionDict.put("dispSelect", 1);
            questionDict.put("codeSelect", true);
        }
        if (isChoiceQuestion || isMatrix) {
            if (isChoiceQuestion) {
                questionDict.put("answerStyle", "custom");
                questionDict.put("vote_setting", false); // 默认答题后展示投票结果: false
                if (QUESTION_TYPE_MULTIPLE.equals(dispType)) {
                    questionDict.put("options_range", Arrays.asList(0, 0)); // 选项个数设置，最少选几项：不限；最多选几项：不限
                }
            }

            questionDict.put("align", "vertical"); // 默认选项排列：竖排
            questionDict.put("options_random", false); // 默认选项顺序：默认
            questionDict.put("group_align", "vertical"); // 默认选项分组排列：竖排
            questionDict.put("optionsBindEffect", false); // 选项绑定设置：false
            questionDict.put("optionsGroupEffect", false); // 选项分组设置: false
            questionDict.put("options_group_random", false); // 默认选项分组顺序：默认
            questionDict.put("showOptionsGroupTitle", true); // 默认选项分组显示组名：true
            if (isMatrixScore) {
                questionDict.put("score_range", Arrays.asList(1, 5));
            }

            if (isMatrix) {
                questionDict.put("firstColWidth", "200");
                questionDict.put("otherColWidth", "");
                questionDict.put("rows_random", false); // 默认矩阵行顺序：默认
                questionDict.put("rows_group_random", false); // 默认矩阵行分组顺序：默认
                questionDict.put("rowsBindEffect", false); // 矩阵行绑定设置: false
                questionDict.put("rowsGroupEffect", false); // 矩阵行分组设置: false
                questionDict.put("showMatrixRWord", false); // 矩阵行分右极文字设置: false
                questionDict.put("swap", false); // 矩阵行行列交行: false
                questionDict.put("unique", false); // 矩阵行答案唯一: false
            }
        }

        if (isBlank) {
            questionDict.put("inputbox_height", "small"); // 输入框高度：小
            questionDict.put("words_number_range", new ArrayList<>());
            questionDict.put("submit_once", false); // 只能提交一次默认：fasle
            if (QUESTION_TYPE_BLANK.getValue().equals(customQtype)) { // 填空题内容格式限制
                questionDict.put("content_type", 0); // 内容限制：不限
            }
            if (QUESTION_CUSTOM_TYPE_BLANK_PHONE.getCustomQtype().equals(customQtype)) { // 手机题
                questionDict.put("open_validate", false);
                questionDict.put("content_type", "mobile");
            } else if (QUESTION_CUSTOM_TYPE_BLANK_EMAIL.getCustomQtype().equals(customQtype)) { // 邮箱
                questionDict.put("content_type", "email"); // 内容限制：不限
            } else if (QUESTION_CUSTOM_TYPE_BLANK_USERNAME.getCustomQtype().equals(customQtype)) { // 姓名
                questionDict.put("content_type", 0); // 内容限制：不限
            }
        }

        if (isMutipileBlank) {
            questionDict.put("options_random", false);
        }

        if (isProportion) {
            questionDict.put("total", 100); // 比重题总分默认100
        }

        if (isAutotable) {
            questionDict.put("defaultRows", 2); // 默认行数：2
            questionDict.put("maxRows", ""); // 最大行数
        }
        if (isScore) {
            questionDict.put("icon", "star");
            if (QUESTION_CUSTOM_TYPE_SCORE_CURSOR_SCORE.getCustomQtype().equals(customQtype)) {
                questionDict.put("icon", "cursor_score");
            }
            if ("star_score".equals(customQtype) || QUESTION_CUSTOM_TYPE_SCORE_CURSOR_SCORE.getCustomQtype().equals(customQtype)) {
                questionDict.put("score_range", Arrays.asList(1, 5));
                questionDict.put("optionsBindEffect", false); // 选项绑定设置：false
                questionDict.put("optionsGroupEffect", false); // 选项分组设置: false
                questionDict.put("options_group_random", false); // 默认选项分组顺序：默认
                questionDict.put("options_random", false); // 默认选项顺序：默认
                JSONObject promptJs = new JSONObject();
                promptJs.put("left_words", "");
                promptJs.put("middle_words", "");
                promptJs.put("right_words", "");
                questionDict.put("prompt_text", promptJs);
            }
            if (QUESTION_CUSTOM_TYPE_SCORE_NPS_SCORE.getCustomQtype().equals(customQtype)) { // NPS题
                questionDict.put("icon", "");
                questionDict.put("score_range", Arrays.asList(0, 10));
                JSONObject promptJs = new JSONObject();
                promptJs.put("left_words", "不可能");
                promptJs.put("middle_words", "有可能");
                promptJs.put("right_words", "极有可能");
                questionDict.put("prompt_text", promptJs);
            }
        }

        if (QUESTION_TYPE_ORDER.getCustomQtype().equals(dispType)) {
            questionDict.put("options_random", false); // 默认选项顺序：默认
            questionDict.put("options_range", Arrays.asList(0, 0)); // 最少选，最多选：默认 不限
            questionDict.put("align", "vertical"); // 选项顺序：默认
        }

        if (QUESTION_TYPE_GEO.getValue().equals(dispType)) {
            questionDict.put("modifyChangeLocation", false); // 不允许手动修正位置: 默认 false
        }
        if (isCity) {
            questionDict.put("info_level", 4); // 默认 详细到"街道信息"
        }
        return questionDict;
    }

    private List<String> checkExcelValid(JSONArray questionList) {
        // 1、检查题目，选项，矩阵行的cid是否重复；
        //    2、检查题目与分组的设置是否匹配；
        List<String> errorMsgs = new ArrayList<>();
        List<String> qQcids = new ArrayList<>();
        for (int i = 0; i < questionList.size(); i++) {
            JSONObject oneQuestion = questionList.getJSONObject(i);
            qQcids.add(oneQuestion.getString("cid"));
        }

        if (!qQcids.isEmpty() && qQcids.size() != new HashSet<>(qQcids).size()) {
            List<String> exists = checkItemExisted(questionList, "cid");
            if (CollectionUtils.isNotEmpty(exists)) {
                errorMsgs.add(String.format("题目cid:%s 重复", String.join(",", exists)));
            }
        }

        JSONObject qcidItemDict = new JSONObject();
        for (int i = 0; i < questionList.size(); i++) {
            JSONObject qDict = questionList.getJSONObject(i);
            qcidItemDict.put(qDict.getString("cid"), qDict);

            String qcid = qDict.getString("cid"); // 题目 cid
            if (!qcid.matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
                errorMsgs.add(String.format("题目cid:%s 格式错误:英文[a-z,A-Z],数字,下划线", qcid));
            }
            if (qcid.contains("__")) {
                errorMsgs.add(String.format("题目cid:%s 题目编号不允许输入双英文下划线", qcid));
            }
            JSONArray items = qDict.getJSONArray("items"); // 选项
            JSONArray rowsItems = qDict.getJSONArray("rows_items"); // 矩阵行
            List<String> exists = checkItemExisted(items, "oid");
            if (CollectionUtils.isNotEmpty(exists)) {
                errorMsgs.add(String.format("题目:%s, 选项cid:%s 重复", qcid, String.join(",", exists)));
            }
            exists = checkItemExisted(rowsItems, "oid");
            if (CollectionUtils.isNotEmpty(exists)) {
                errorMsgs.add(String.format("题目:%s, 矩阵行cid:%s 重复", qcid, String.join(",", exists)));
            }
        }

        // 检查选项分组，选项绑定 排他设置的选项的oid是否与选项一致；
        // 矩阵行分组，矩阵行绑定的items的矩阵行的oid，与矩阵行一致;
        for (int i = 0; i < questionList.size(); i++) {
            JSONObject qDict = questionList.getJSONObject(i);
            String qcid = qDict.getString("cid");
            JSONArray items = Optional.ofNullable(qDict.getJSONArray("items")).orElse(new JSONArray()); // 选项
            JSONArray optionGroupList = Optional.ofNullable(qDict.getJSONArray("option_group_list")).orElse(new JSONArray()); // 选项分组
            JSONArray optionBindList = Optional.ofNullable(qDict.getJSONArray("option_bind_list")).orElse(new JSONArray()); // 选项绑定
            JSONArray optionExcludeList = Optional.ofNullable(qDict.getJSONArray("option_exclude_list")).orElse(new JSONArray()); // 排他设置
            JSONArray rowsItems = Optional.ofNullable(qDict.getJSONArray("rows_items")).orElse(new JSONArray()); // 矩阵行
            JSONArray rowGroupList = Optional.ofNullable(qDict.getJSONArray("row_group_list")).orElse(new JSONArray()); // 矩阵行分组
            JSONArray rowBindList = Optional.ofNullable(qDict.getJSONArray("row_bind_list")).orElse(new JSONArray()); // 矩阵行绑定

            List<String> ocidList = new ArrayList<>();
            List<String> rcidList = new ArrayList<>();
            for (int j = 0; j < items.size(); j++) {
                JSONObject item = items.getJSONObject(j);
                ocidList.add(item.getString("oid"));
            }
            for (int j = 0; j < rowsItems.size(); j++) {
                JSONObject item = rowsItems.getJSONObject(j);
                rcidList.add(item.getString("oid"));
            }

            // 选项oid格式检查
            for (int j = 0; j < items.size(); j++) {
                JSONObject oitem = items.getJSONObject(j);
                String oid = oitem.getString("oid");
                String opTitle = oitem.getString("title");
                if (StringUtils.isEmpty(oid)) {
                    errorMsgs.add(String.format("题目:%s, 选项cid:%s 不能为空", qcid, oid));
                }
                if (StringUtils.isEmpty(opTitle)) {
                    errorMsgs.add(String.format("题目:%s, 选项cid:%s 的标题不能为空", qcid, oid));
                }
                if (!oid.matches("^\\d+$")) {
                    errorMsgs.add(String.format("题目:%s, 选项cid:%s, 格式错误:仅数字", qcid, oid));
                }
            }

            // 选项组 校验
            boolean hasOtherGroup = false;
            List<String> opGroupOidKeys = new ArrayList<>();
            JSONObject oidGroupDict = new JSONObject(); // 选项：选项分组index
            JSONObject ogroupOids = new JSONObject(); // 选项分组：选项oid
            for (int j = 0; j < optionGroupList.size(); j++) {
                Integer ogIndex = j + 1;
                JSONObject itemDict = optionGroupList.getJSONObject(j);
                String opgTitle = Optional.ofNullable(itemDict.getString("title")).orElse("");
                JSONArray itemsJs = Optional.ofNullable(itemDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject oitemDict = itemsJs.getJSONObject(k);
                    String opoid = oitemDict.getString("oid");

                    oidGroupDict.put(opoid, ogIndex); // 选项：选项分组index
                    JSONArray oids = ogroupOids.getJSONArray(ogIndex.toString()); // 选项分组：选项oid
                    if (oids == null) {
                        oids = new JSONArray();
                        ogroupOids.put(ogIndex.toString(), oids);
                    }
                    oids.add(opoid);

                    if (!ocidList.contains(opoid)) {
                        errorMsgs.add(String.format("题目:%s, 选项分组:%s, 设置的选项cid: %s 未找到", qcid, opgTitle, opoid));
                    }
                    if (!opGroupOidKeys.contains(opoid)) {
                        opGroupOidKeys.add(opoid);
                    } else {
                        errorMsgs.add(String.format("题目:%s, 选项分组:%s, 设置的选项cid: %s 重复", qcid, opgTitle, opoid));
                    }
                }
                // 其他分组 id 命名:qgid_OTHER_GROUP
                if (ogIndex.equals(optionGroupList.size()) && "其他分组".equals(opgTitle)) {
                    hasOtherGroup = true;
                }
            }

            if (!optionGroupList.isEmpty() && !hasOtherGroup) { // 选项分组 没有其他分组
                errorMsgs.add(String.format("题目:%s, 选项分组设置错误: 缺少 其他分组", qcid));
            }
            if (CollectionUtils.isNotEmpty(opGroupOidKeys)) { // 选项分组 没有其他分组
                Collections.sort(opGroupOidKeys);
            }

            if (!optionGroupList.isEmpty() && (CollectionUtils.disjunction(ocidList, opGroupOidKeys).size() > 0)) { // 选项没有设置到选项分组上
                errorMsgs.add(String.format("题目:%s, 选项分组设置错误: %s 未设置到选项分组", qcid, String
                        .join(",", CollectionUtils.disjunction(ocidList, opGroupOidKeys))));
            }

            // 选项绑定 校验
            JSONObject oidBindGroupOps = new JSONObject(); // 绑定的选项 分组字典 bind_group_index: op
            JSONObject obindTitleDict = new JSONObject(); // 绑定组ID：绑定组title
            for (int j = 0; j < optionBindList.size(); j++) {
                int obIndex = j + 1;
                JSONObject bindDict = optionBindList.getJSONObject(j);
                String opgTitle = Optional.ofNullable(bindDict.getString("title")).orElse("");
                obindTitleDict.put(Integer.toString(obIndex), opgTitle);
                JSONArray itemsJs = Optional.ofNullable(bindDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject oitemDict = itemsJs.getJSONObject(k);
                    String opoid = oitemDict.getString("oid");
                    if (!ocidList.contains(opoid)) {
                        errorMsgs.add(String.format("题目:%s, 选项绑定:%s, 设置的选项cid: %s 未找到", qcid, opgTitle, opoid));
                    }

                    String obindStr = oidBindGroupOps.getString(opoid);
                    if (StringUtils.isEmpty(obindStr)) {
                        oidBindGroupOps.put(opoid, obindStr);
                    } else {
                        errorMsgs.add(String.format("题目:%s, 选项绑定:%s, 设置的选项cid: %s 重复", qcid, opgTitle, opoid));
                    }
                }
            }

            // 检查选项绑定的选项是否选项分组冲突
            for (int j = 0; j < optionBindList.size(); j++) {
                JSONObject bindDict = optionBindList.getJSONObject(j);
                Set<String> ogroupIndexs = new HashSet<>();
                String opgTitle = Optional.ofNullable(bindDict.getString("title")).orElse("");
                JSONArray itemsJs = Optional.ofNullable(bindDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject oitemDict = itemsJs.getJSONObject(k);
                    String opoid = oitemDict.getString("oid");
                    String ogroupIndex = oidGroupDict.getString(opoid); // 选项所属分组
                    String obindStr = oidBindGroupOps.getString(opoid); // 选项所在的绑定组
                    if (StringUtils.isNotEmpty(ogroupIndex) && StringUtils.isNotEmpty(obindStr)) {
                        ogroupIndexs.add(ogroupIndex);
                    }
                    if (ogroupIndexs.size() > 1) {
                        // 绑定选项所属组超过1，说明不同选项分组的选项绑定在一个组下
                        errorMsgs.add(String.format("题目:%s, 选项绑定:%s, 设置的选项cid 只能是一个分组内选项", qcid, opgTitle));
                    }
                }
            }
            // 选项排他 校验
            for (int j = 0; j < optionExcludeList.size(); j++) {
                int oeIndex = j + 1;
                JSONObject optionExcludeDict = optionExcludeList.getJSONObject(j);
                String opgTitle = Optional.ofNullable(optionExcludeDict.getString("title")).orElse("");
                obindTitleDict.put(Integer.toString(oeIndex), opgTitle);
                JSONArray itemsJs = Optional.ofNullable(optionExcludeDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject oitemDict = itemsJs.getJSONObject(k);
                    String opoid = oitemDict.getString("oid");
                    if (!ocidList.contains(opoid)) {
                        errorMsgs.add(String.format("题目:%s, 选项排他:%s, 设置的选项cid: %s 未找到", qcid, opgTitle, opoid));
                    }
                }
            }

            // 矩阵行oid格式检查
            for (int j = 0; j < rowsItems.size(); j++) {
                JSONObject item = rowsItems.getJSONObject(j);
                String rowoid = item.getString("oid");
                String opTitle = Optional.ofNullable(item.getString("title")).orElse("");
                if (StringUtils.isEmpty(rowoid)) {
                    errorMsgs.add(String.format("题目:%s, 矩阵行cid:%s 不能为空", qcid, rowoid));
                } else if (!rowoid.matches("^[a-zA-Z0-9_]*$")) { // 题目cid格式校验:英文[a-z,A-Z],数字,下划线
                    errorMsgs.add(String.format("题目:%s, 矩阵行cid:%s, 格式错误:英文[a-z,A-Z],数字,下划线", qcid, rowoid));
                }
                if (StringUtils.isEmpty(opTitle)) {
                    errorMsgs.add(String.format("题目:%s, 矩阵行cid:%s 的标题不能为空", qcid, rowoid));
                }
            }

            // 矩阵行分组 校验
            boolean hasOtherRowgroup = false; // 其他矩阵行分组
            List<String> rowGroupOidKeys = new ArrayList<>();
            JSONObject rowGroupDict = new JSONObject(); // 矩阵行：矩阵行分组index
            JSONObject rgroupOids = new JSONObject(); // 矩阵行分组：矩阵行oid
            for (int j = 0; j < rowGroupList.size(); j++) {
                int rgIndex = j + 1;
                JSONObject rgItemDict = rowGroupList.getJSONObject(j);
                String rowgTitle = Optional.ofNullable(rgItemDict.getString("title")).orElse("");
                JSONArray itemsJs = Optional.ofNullable(rgItemDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject rowitemDict = itemsJs.getJSONObject(k);
                    String rowoid = rowitemDict.getString("oid");
                    rowGroupDict.put(rowoid, rgIndex); // 矩阵行：矩阵行分组index
                    JSONArray rowids = rgroupOids.getJSONArray(String.valueOf(rgIndex));
                    if (CollectionUtils.isEmpty(rowids)) {
                        rowids = new JSONArray();
                        rgroupOids.put(String.valueOf(rgIndex), rowids);
                    }
                    rowids.add(rowoid);

                    if (!rcidList.contains(rowoid)) {
                        errorMsgs.add(String.format("题目:%s, 矩阵行分组:%s, 设置的矩阵行cid: %s 未找到", qcid, rowgTitle, rowoid));
                    }

                    if (!rowGroupOidKeys.contains(rowoid)) {
                        rowGroupOidKeys.add(rowoid);
                    } else {
                        errorMsgs.add(String.format("题目:%s, 矩阵行分组:%s, 设置的矩阵行cid: %s 重复", qcid, rowgTitle, rowoid));
                    }
                }

                // 其他分组 id 命名:qgid_OTHER_GROUP
                if (rgIndex == rowGroupList.size() && "其他分组".equals(rowgTitle)) {
                    hasOtherRowgroup = true;
                }
            }
            if (!rowGroupList.isEmpty() && !hasOtherRowgroup) { // 矩阵行分组 没有其他分组
                errorMsgs.add(String.format("题目:%s, 矩阵行分组设置错误: 缺少 其他分组", qcid));
            }

            if (!rowGroupList.isEmpty() && (CollectionUtils.disjunction(rcidList, rowGroupOidKeys).size() > 0)) { // 选项没有设置到选项分组上
                errorMsgs.add(String.format("题目:%s, 矩阵行分组设置错误: %s 未设置到矩阵行分组", qcid, String
                        .join(",", CollectionUtils.disjunction(rcidList, rowGroupOidKeys))));
            }

            // 矩阵行绑定 校验
            JSONObject rowidBindGroupOps = new JSONObject(); // 绑定的矩阵行 分组字典 bind_group_index: op
            JSONObject rowBindTitleDict = new JSONObject(); // 绑定组ID：绑定组title
            for (int j = 0; j < rowBindList.size(); j++) {
                int rbIndex = j + 1;
                JSONObject rowBindDict = rowBindList.getJSONObject(j);
                String rowTitle = Optional.ofNullable(rowBindDict.getString("title")).orElse("");
                rowBindTitleDict.put(String.valueOf(rbIndex), rowTitle);
                JSONArray itemsJs = Optional.ofNullable(rowBindDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject rowitemDict = itemsJs.getJSONObject(k);
                    String rowoid = rowitemDict.getString("oid");
                    if (!rcidList.contains(rowoid)) {
                        errorMsgs.add(String.format("题目:%s, 矩阵行绑定:%s, 设置的选项cid: %s 未找到", qcid, rowTitle, rowoid));
                    }

                    String rbindstr = rowidBindGroupOps.getString(rowoid);
                    if (StringUtils.isEmpty(rbindstr)) {
                        rowidBindGroupOps.put(rowoid, String.valueOf(rbIndex));
                    } else {
                        errorMsgs.add(String.format("题目:%s, 选项绑定:%s, 设置的选项cid: %s 重复", qcid, rowTitle, rowoid));
                    }
                }
            }

            // 检查矩阵行绑定的选项是否选项分组冲突
            for (int j = 0; j < rowBindList.size(); j++) {
                JSONObject bindDict = rowBindList.getJSONObject(j);
                Set<String> rgroupIndexs = new HashSet<>();
                String rbgTitle = Optional.ofNullable(bindDict.getString("title")).orElse("");
                JSONArray itemsJs = Optional.ofNullable(bindDict.getJSONArray("items")).orElse(new JSONArray());
                for (int k = 0; k < itemsJs.size(); k++) {
                    JSONObject oitemDict = itemsJs.getJSONObject(k);
                    String rowoid = oitemDict.getString("oid");
                    String rgroupIndex = rowGroupDict.getString(rowoid); // 矩阵行所属分组
                    String rbindStr = rowidBindGroupOps.getString(rowoid); // 选项所在的绑定组
                    if (StringUtils.isNotEmpty(rgroupIndex) && StringUtils.isNotEmpty(rbindStr)) {
                        rgroupIndexs.add(rgroupIndex);
                    }
                    if (rgroupIndexs.size() > 1) {
                        // 绑定选项所属组超过1，说明不同选项分组的选项绑定在一个组下
                        errorMsgs.add(String.format("题目:%s, 矩阵行绑定:%s, 设置的矩阵行cid 只能是一个分组内选项", qcid, rbgTitle));
                    }
                }
            }

            JSONArray relatedRefs = qDict.getJSONArray("relatedRefs"); // 选项关联
            if (relatedRefs != null && !relatedRefs.isEmpty()) {
                // 处理选项关联
                for (int k = 0; k < relatedRefs.size(); k++) {
                    JSONObject relatedDict = relatedRefs.getJSONObject(k);
                    String sourceQcid = Optional.ofNullable(relatedDict.getString("sourceQcid")).orElse("");
                    Object sourceQ = qcidItemDict.get(sourceQcid);
                    if (sourceQ == null) {
                        errorMsgs.add(String.format("题目:%s, 关联的%s题 未找到", qcid, sourceQcid));
                    }
                }
            }
        }
        return errorMsgs;
    }

    private List<String> checkItemExisted(JSONArray items, String key) {
        // 检查item的oid是否有重复
        if (items == null || items.isEmpty()) {
            return new ArrayList<>();
        }
        List<String> cids = new ArrayList<>();
        List<String> exists = new ArrayList<>();
        for (int i = 0; i < items.size(); i++) {
            JSONObject item = items.getJSONObject(i);
            String cid = item.getString(key);
            if (StringUtils.isEmpty(cid)) {
                continue;
            }
            if (!cids.contains(cid)) {
                cids.add(cid);
            } else {
                exists.add(cid);
            }
        }
        return exists;
    }

    private JSONObject getStructDataSample() {
        String attr = ResourceUtil.readUtf8Str(STRUCT_DEFAULT_DATA_FILE_PATH);
        //String urlPrefix = StrUtil.removeSuffix(ossService.getCdn() + ossService.getFilePrefix(), "/");
        String urlPrefix = StrUtil.removeSuffix(storageService.getPropertiesValueBy(null, "domain") + storageService.getPropertiesValueBy(null, "base-path"), "/");
        return JSON.parseObject(String.format(attr, urlPrefix, urlPrefix, urlPrefix));
    }

    /**
     * @param projectId 问卷id
     * @param gid       问题gid
     * @return int, int: 最小分值，最大分值
     */
    @Override
    public JSONObject getScoreRange(String projectId, Integer gid) {
        JSONObject res = new JSONObject();
        QdesProject project = qdesProjectMapper.selectById(projectId);
        if (project.getStatus().equals(PROJECT_STATUS_CREATED.getNumber())) {
            try {
                project = getCurrentProject(projectId);
            } catch (JsonProcessingException e) {
                log.error("JsonProcessingException", e);
            }
        }
        QdesQuestion question = qdesQuestionMapper.selectOne(new LambdaQueryWrapper<QdesQuestion>()
                .eq(QdesQuestion::getStructId, project.getStructId()).eq(QdesQuestion::getGid, gid).last("limit 1"));
        if (Objects.isNull(question)) {
            throw new BusinessException(String.valueOf(ReqErrorCodeEnum.NOT_FOUND), String.format("Question[structID={%s},gid={%s}] not found", project.getStructId(), gid));
        }
        JSONObject data = JSON.parseObject(question.getData());
        if ("proportion".equals(data.get("custom_qtype"))) {
            //比重题
            Integer prop_min = (Integer) data.getOrDefault("min", 0);
            Integer prop_max = (Integer) Optional.ofNullable(data.getOrDefault("max", 0)).orElse(data.getOrDefault("total", 0));
            res.put("minScore", prop_min);
            res.put("maxScore", prop_max);
            return res;
        }
        List<Integer> score_range = data.getJSONArray("score_range").toJavaList(Integer.class);
        if (CollUtil.isEmpty(score_range)) {
            res.put("minScore", 1);
            res.put("maxScore", 5);
            return res;
        }
        res.put("minScore", score_range.get(0));
        res.put("maxScore", score_range.get(1));
        return res;
    }
}
